Commit Graph

252 Commits

Author SHA1 Message Date
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
raykholo 189b47c750 Chamfer and Fillet refuse a point where several edges meet (#5173) (#5177) 2026-10-26 10:42:59 +00:00
Jesper We 0c4eb458df macOS 3D mouse: unregister from the driver when the app ends, so the next launch gets the mouse (#6114) (#6115)
* claim: #6114

* macOS 3D mouse: unregister from the driver when the app ends, so the next launch gets the mouse (#6114)
2026-10-26 10:42:25 +00:00
Victor Santos Oliveira b3cbbe7bd5 CLI: reject incomplete and unsupported run/eval options (#6918) (#6919)
* claim: #6918 validate run and eval options

* fix(cli): reject invalid run and eval options (#6918)
2026-10-26 10:38:56 +00:00
raykholo 36a60a70d9 Graphics: 1x anti-aliasing where the multisampled view draws nothing (#5819, #5870) (#5832) 2026-10-25 22:10:04 +00:00
raykholo 0344a41dc4 Open and insert a DXF outside a sketch (#2052) (#4635) 2026-10-25 21:03:31 +00:00
JK3D 492bba9242 Control channel: write each reply in one piece (#6389) (#6390)
* claim: #6389

* perf: write each control reply to the socket in one piece

The control server wrote each reply with writeln!(out, "{reply}") on the unbuffered TcpStream.
Formatting a serde_json::Value hands the writer every string, number and punctuation mark
separately, so each became its own write system call. A reply of a few megabytes (component.list
on a design with thousands of components) took about half a second to send.

Every reply is now serialized to one buffer and written at once. Writing one large Value to a
loopback TcpStream: 1.79 MB took 1,052 ms with writeln! and 4.9 ms with one write_all.

The test counts the write calls for a 2000-component reply (one) and reads it back.
2026-10-25 17:55:14 +00:00
raykholo b1da434458 Graphics: choose the graphics card, honour WGPU_ADAPTER_NAME (#5986) (#6010) 2026-10-25 16:49:57 +00:00
raykholo 4cd9a4f4d4 Web app: open a design from a link, and an embed mode (#6252) (#6253)
* claim: #6252

* Open hosted designs in the web app and add an embed layout

Fixes #6252.

Fetch HTTPS design links with bounded reads and open them as unsaved copies.
Keep model panels and joint controls in embeds without browser persistence,
application chrome, or unload prompts. Add a full-app link and iframe docs.

Cover query parsing, hostile URLs, size limits, and the compact layout.

* Translate the linked-design messages into Japanese and Chinese
2026-10-25 14:40:01 +00:00
bherbruck 093571cfe4 Connect AI apps at start-up, with a preference to turn it off (#6388)
At launch the app adds the solidcraft entry to each installed AI client off the window thread,
names newly connected clients once, and refreshes a stale path. Settings > Assistant has
"Make SolidCraft available to AI apps on this computer" (on by default); off removes the entries it
added. Entries the user changed are left alone; automation runs never touch the files.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:51:55 +00:00
Colin Faulkingham 16a5254e97 Fix macOS fullscreen application-bar interaction (#3643) (#3690)
* claim: #3643

* fix: keep macOS fullscreen application bar clickable (#3643)

* test: load bundled fonts for fullscreen regression
2026-10-25 12:49:02 +00:00
bherbruck dfde1e032d mcp setup: add SolidCraft to the AI clients on this computer; plain mcp finds a late app (#6379)
solidcraft-cli mcp setup (alias install) and Help > Connect AI assistants edit each installed
client's own configuration in place, with this program's absolute path: OpenCode, Claude Desktop,
Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed and VS Code. Only the solidcraft
entries change; a stale path is refreshed and an entry the user changed is left alone.

Plain mcp keeps looking for the app while its headless session has no work, and says on every
tool result that it is headless when the app was not found.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:57:51 +00:00
raykholo cc1e654690 Warn when a design is open elsewhere or changed on disk (#5467) (#5468)
* claim: #5467

* Warn when a design is open elsewhere or changed on disk (#5467)

The desktop app keeps a lock file next to each open design
(.~<file>.lock: computer, user, version, opened and refreshed times),
refreshes it every minute and removes it on close; a lock not
refreshed for three minutes is stale and ignored. Opening a design
that another SolidCraft has open asks: Open Read-Only, Open Anyway or
Cancel. A read-only design saves only to another file.

A design remembers its file's size and modification time when it is
opened or saved. Save over a file another program or computer changed
since asks: Save a Copy (the default), Overwrite or Cancel.

Programmatic calls never ask: doc.open reports open_elsewhere and takes
read_only, and file.save over a changed file returns an error until it
is given overwrite: true. ui.documents answers both questions.
2026-10-25 03:34:20 +00:00
raykholo 138bb0261c Windows OpenGL: use the system title bar so the picture lines up with clicks (#2063) (#4403)
* claim: #2063

* Windows OpenGL: use the system title bar so the picture lines up with clicks (#2063)

On Windows with only OpenGL to start, the window opens with the system
title bar instead of using the application bar as its title bar. The
window without OS decorations keeps its caption style while having no
frame, and some OpenGL drivers (older Intel HD/UHD) show the picture a
caption's height lower than the window, so clicks land above what they
point at. Direct3D 12 and Vulkan keep the custom title bar.

The choice is made before the window opens, from the backends wgpu
starts, so the window never changes its frame while OpenGL draws into it.
2026-10-25 03:19:34 +00:00
bherbruck a150822d9b Put solidcraft-cli on PATH from the AppImage and the macOS app (#6347)
The AppImage writes a ~/.local/bin/solidcraft-cli launcher from its desktop-integration step
(same opt-outs, never over a file it did not write). The macOS app links the CLI into
/opt/homebrew/bin or /usr/local/bin at launch when one is writable, and Help > Install
solidcraft-cli command does it with an administrator prompt otherwise. Snap and Flatpak are documented.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:45:45 +00:00
ivisaf 765c5fd143 Pictures drawn off the window thread finish before the process exits (#6062) (#6063)
A Home picture still being drawn by the detached saved-thumbnail thread
could be on the GPU as the process exited, and the process died with
SIGSEGV inside the graphics driver. capture::close stops new pictures
and waits for those in flight (the app calls it in on_exit), and
dropping SolveApp waits for the Home picture threads it started.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:40 +00:00
neosandeep24 9ad96fb81b Home: keep a removed recent design removed (#4660) (#4718)
Removing a design from the Start page's recent list only changed memory, and on the web sync_recents() rebuilt the list from stored designs on each launch, adding it back. Track removed designs in a persisted 'forgotten' set that sync_recents honours; opening or saving a design, or deleting/renaming its file, clears it.
2026-10-25 01:26:31 +00:00
Matthew Knutson 05d43d3542 Windows: the taskbar button shows the mark when the first frames are slow (#6391)
The taskbar asks a window for its icon once, when the window appears. eframe
sets the icon while the window is still hidden and shows it after the first
frame; when that frame is slow (safe mode's software renderer) or startup kept
the UI thread busy long enough for Windows to mark the window as not
responding (building the viewport pipelines), the request isn't answered in
time, the taskbar falls back to the window class's icon, and winit's class has
none: the button showed Windows' generic icon for as long as the window was
open.

Give the class the exe's embedded icon (resource 1) before the window is shown.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:26 +00:00
Tyler Chiriboga 2ff17c0fcb 3D mouse: follow the device that moves, not the first one found (#3988) (#4339)
* claim: #3988 (3D mouse: follow the device that moves, not the first one found)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: follow the device that moves, not the first one found (#3988)

On Windows and macOS the reader opened the first 3D-mouse interface it
could and read it until it failed. A 3Dconnexion Universal Receiver with
nothing paired opens fine, has a valid layout and never fails, so it kept
the reader for good. A SpaceMouse on a cable next to it was never read
("Not calibrated (hid 256f:c652)", every axis at zero).

The reader now opens every candidate and follows the one in use. The
first is followed from the start, as before. Another takes over once it
shows use (an axis off zero or a button down) while the followed one has
shown none for STALE_AFTER, so two devices moved together don't trade
the reading. A candidate that fails is dropped; if it was the followed
one the reading resets and the next is read. With one device the reader
behaves as before, down to the 100 ms read timeout. Several take 10 ms
turns, and reports queue in the system meanwhile.

The selection is `follow()`, written against a small `ReportSource`
trait, so it is tested with simulated devices on every platform: an idle
receiver next to a SpaceMouse, two devices in use at once, and devices
failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: a press that makes a device the one in use counts as a press

Since #3825 a HID device's first button report after it connects is its
baseline: a button down in it is held, not pressed. A device taking over
from another one was connected at that moment, so the press that took
over was swallowed. With an idle Universal Receiver found first, that is
the first button press on the SpaceMouse.

follow() now keeps the buttons each candidate last reported while another
was read, and the reader hands them to the reading as the baseline when
that candidate takes over. A device that reported no buttons before it
took over keeps the held-not-pressed rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:18:34 +00:00
raykholo 4cd1a51f48 Assistant: a request timeout per endpoint for slow models (#5009) (#5010)
* claim: #5009

* Assistant: a request timeout per endpoint for slow models (#5009)

An OpenAI-compatible endpoint now stores how long one model request may
take (request_timeout_s, 300 s by default, clamped to 30 s ..= 1 h). It
is set in the Add agent dialog and in the endpoint's entry under Agents
(a running conversation uses a change from its next request), through
ui.agent_chat add_agent, and reported in the chat's state. A request
that runs out of time says the limit and where to raise it instead of
"timeout: global". The desktop HTTP cap goes from 900 s to an hour.

The tool-call timeout stays 330 s: it covers SolidCraft's own tool work
and the user's Allow, never the model.

* Assistant settings: line up an endpoint's Request timeout with Permissions

The label column of an agent's entry is wide enough for "Request timeout",
so the label isn't cut and both rows share one column.
2026-10-24 22:24:42 +00:00
Nathalie Schönwetter b702ce8c7d fix: start the window on Intel Macs without wgpu's indirect-call validation (#3024) (#4808)
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:52:55 +00:00
raykholo 0520910bfd Control: name the token file and spot a stale one when a call is unauthorized (#4664) (#4670)
* claim: #4664

* Control: name the token file and spot a stale one when a call is unauthorized (#4664)

When the app refuses a token that the MCP client read from a token file,
the error now names that file, when it was written and how long ago,
without printing the token. The app also writes `<port>.pid` beside the
token file (its process id and, on Linux, its start time) and removes
both at exit; when that process has exited, the error says the token
file is stale. The process check runs only after a refusal, and the
token file itself stays one line, so existing clients keep working.

Document force-quit leftovers and the MSIX AppData redirection.

* Bench assistant: read the token out of the new ClientToken

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:28:12 +00:00
cronos 4f4b1fc2d2 Sketch: select edge midpoints for horizontal and vertical constraints (#5159) 2026-10-24 16:15:45 +00:00
selune 9e6e0b8df1 Render workspace: complete experimental ribbon, resources and effects integration (#5493) (#5494)
* wip(render): reconcile completed material and image pipeline source with 0.20 (#5493)

Source-only integration reference for staged review, not a merge-ready aggregate.
Donor: 0xlillith/solidcraft@aea20b0115.
Upstream: 858e5995d6.

Retain original CPU/material contracts and explicit GPU donor provenance in
docs/render-materials-lab.md (#2067, #2191, #2780, #3306). Preserve released
format migrations and current navigation/startup/shell behavior. New saved
records use pending descriptors. The physical-card descriptor follows the
maintainer's material-engineering-cards naming request, with a legacy-lab
format-29 regression. Replace raw new panel hosts with shared containers.

Repository checks, complete workspace/all-target Clippy, and the independent
physical/resource authoring checks pass for this source. The fresh full local
CI invocation has reached test compilation and is not yet complete. Do not
transfer donor native output or prior test totals to this integration.

Generated review screenshots, video, package ZIP and machine logs remain at
the immutable personal evidence pin. Uncommitted geometric displacement is
backed up separately, not silently included. No kernel/vendor change, direct
main write, release or independent contributor-branch rewrite.

Co-authored-by: Christian Svensson <blue@cmd.nu>

* test(cli): pin the Compact camera-sample admission diagnostic (#5493)

The bounded request still refuses 1024x1024x64 samples before host initialization and writes no output. Update only the stale expected diagnostic to the existing Compact budget wording; production limits and code are unchanged. Exact-tree CLI regression suite: 14 passed, 0 failed. The full repository gate has been invoked separately and is not yet complete.

* Normalize UI container violation paths across platforms

Report repository-relative paths with forward slashes so diagnostics and the existing nested-package regression assertions agree on Windows and Unix. Keep the container detection rules unchanged.

Validation: official ui-containers gate and all seven focused xtask tests passed; rustfmt and diff checks passed. Only xtask/dependencies were compiled; no application build or heavy tests.
(cherry picked from commit 44b58d8e10)

* fix(render): preserve camera contracts and correct acceptance regressions (#5493)

Preserve Design view.apply semantics with a separate strict Render camera command. Retain existing translations when English-only aliases collide; add material-axis entries and camera-specific pitch labels. Compare complete validated engineering cards and retain job handles before cancellation.

Focused and native acceptance is pinned to tree f8a423b21f. Full CI has been invoked separately and is not claimed green. Landing follows the render integration policy in #5493.

* fix(render): resolve showcase licence and upstream resource hygiene (#5493)

Record SychPL's explicit MIT OR Apache-2.0 grant, retaining source/model bytes and the Ubuntu lettering notice. Remove 80 absent attribution rows, personal fork records, dead delivery attributes and three fork/resource-only workflows. Existing xtask assets now performs bounded offline hash checks and rejects orphan render attribution; no parallel draft CI or package installation. Original authoring reproduction remains explicit. Scoped xtask tests, assets, Clippy, formatting and prepared SELENE validation pass; no application or renderer-wide result is inferred.

* fix(render): align saved bounds and progressive sample resolution (#5493)

Address review r4364211669/r4364211677/r4364211681/r4364211691/r4364211695. Persisted light/frame domains respect the existing 1e9 document bound; related LED/rectangle/direction cases use the same bound. Workstation primary resolve shares the 4096 admission cap and film counters use checked conversions. Exact candidate: 485 CPU render tests, 21 render document tests and strict scoped Clippy pass, including positive boundary round trips and 2048/4096 end-to-end checkpoints. No released format/migration edits, relaxed test limits or full-stack acceptance claim.

* wip(render): isolate optional resources and harden sample handling (#5493)

Review work in progress: opt-in encoded render library with reproducible runtime catalog exports; native-only robust dependency through the workspace; checked bloom slices; per-sample point-light diagnostics; independent area-light spatial/film keys. Resource-module binaries in both configurations, generation, Cargo production feature graphs and attribution checks pass. Full engine/render regression and combined acceptance are pending: do not merge this donor as a constituent or treat scoped resource proof as runtime acceptance.

* fix(gpu): preserve donor allocations through the scope-safety repair (#5722)

Port the native-validated #5723 scope-cleanup correction into the experimental donor, retaining its adaptive, volume, aging, photographic and bloom allocations. Same reverse-drain helper and regression cases; the donor combination still needs its own compile/test gate and is not covered retroactively by render-base results.

* claim: #1369

* Reject unknown pending concepts after format folding (#1369)

* test(render): close bounded sampling and bloom review regressions (#5493)

Validate the optional-resource/lighting/dependency donor at its exact tree. Required Vulkan GPU 127/0/3; CPU renderer 487/0; engine Render 72/0; document render 21/0; lean library 7/0; strict scoped Clippy, assets/layers/lists/containers and lean-engine WASM pass. Preserve original failed controller/typing attempts. This is the draft donor/disposition source, not a replacement for constituent render PRs or final main acceptance.

* wip(render): preserve stable aging target validation candidate

Exact staged source from prior review work on donor 3b892eab5e. This is a recovery/review checkpoint, NOT accepted delivery. An actual command-path regression currently fails because the selected executor refuses ordered surface aging. No assertion, capability or admission limit is weakened. Native/UI/full combination acceptance remains pending. Saved/runtime split adds one pending descriptor and retains original file scope; no main/render branch is changed.

* test(render): preserve numeric setup intent at the admitted coordinate bound

Follow up jyviko review on #5494 without weakening the corrected saved-coordinate contract. The positive clicked fixture uses 1e9 instead of inadmissible 2e9; explicit out-of-range cases still refuse, and the full saved setup round-trips through JSON and the native container. Exact-source Render workspace 95/0 and strict all-target UI Clippy pass. This does not incorporate the separately failing stable-aging candidate or claim full current-render acceptance.

* test(render): use the real GPU executor for saved aging

Move successful native cold-reopen Check/Start coverage from the CPU-backed protocol fixture into the production GPU executor tests. Verify capture-index changes preserve the stable target and that enabling aging changes both PNG and raw EXR output. Retain explicit, allocation-free refusal coverage for executors without aging support. Production capabilities and admission checks are unchanged.

* fix(render): persist stable identities for targeted aging (#5493)

Separate saved RenderBodyTarget identities from captured mesh ordinals. Reject legacy ambiguous targets, resolve Check/Start after visibility changes and preserve native reopening, copies and undo. Unsupported executors still refuse; real AMD Vulkan regression completes saved aging after reordered capture.

Source tree 6b84c8ad passes document 23, engine Render 75, GPU effects 5 and Render UI 95 with no failures; scoped strict Clippy, WASM and repository gates pass. Fresh CLI/native reopen and clean/aged GPU exports pass with identical physical totals and inspected actual images. Whole current-render integration is separate.

* fix(render): reconcile combined CPU/GPU contracts on current upstream

Unify captured resources/status and CPU-vs-GPU request schemas after the real two-parent integration; remove obsolete duplicate Vulkan selector and retain combined platform-filter regression. Deduplicate only identical or superseded test definitions, retaining the stronger card metadata and reserved-executor assertions. No assertions, admission limits or feature refusals are weakened.

All targets of render dependencies, GPU, engine, UI, CLI and native app pass cargo check with raytrace-cpu and bundled-render-library at tree c70d00d1. Native executable tests and full current-source acceptance remain pending. This is a private integration rehearsal for subsequent constituent PRs, not an aggregate official render/main merge.

* test(render): retain one complete host lifetime regression set

Deduplicate byte-identical DropProbe/task-lifetime tests and retain the adapted environment/texture ownership regressions with every assertion and complete memory estimate. Keep the registered-geometry admission regression once; only struct field order differed. Replace a one-element diagnostic loop with its equivalent block, preserving all wrap/width assertions.

Test-only integration repair. Formatting and diff checks pass; refreshed executable validation is required. No production behavior, limits, assertions or ignores changed.

* fix(doc): recognize the proven format-29 card descriptor alias

Normalize only the historical format-29 physical-material-cards spelling, and only when material-engineering-cards is registered. Admission and migration de-duplication use the same identity. Unknown concepts, wrong versions and incapable readers still refuse before mutation. Six controlled-history Rust regressions and actual CLI migration/no-clobber acceptance pass. No version, released migration or CI changes. Refs #5793, #1369, #5493.

* test(render): preserve grounding refusal before explicit instance moves

Update the two Render binding fixtures for upstream first-occurrence grounding. Verify grounded movement refuses without changing document/history, then explicitly unground through the public command before the intended move. Retain every target, resource, copy and persistence assertion. Refs #5493.

---------

Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: cfaulkingham <colin.faulkingham@gmail.com>
2026-10-24 14:21:59 +00:00
selune b7b25f9e2b Render workspace: first basic delivery to main (#6050)
* Normalize UI container violation paths across platforms (#5599)

Report repository-relative paths with forward slashes so diagnostics and the existing nested-package regression assertions agree on Windows and Unix. Keep the container detection rules unchanged.

Validation: official ui-containers gate and all seven focused xtask tests passed; rustfmt and diff checks passed. Only xtask/dependencies were compiled; no application build or heavy tests.

(cherry picked from commit 44b58d8e10)

Scoped render-branch prerequisite from #4507, coordinated under #5493. No unrelated translation/kernel changes are included. Fresh Windows fmt, seven ui_containers regressions and the production container gate pass on render base 3a6153d18f. Full CI is reserved for final render-to-main acceptance under the maintainer policy.

Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>

* Preserve translated source-label aliases (#5619)

Render integration prerequisite for #5493. Fixes #5618. Retain stable-ID fallback and translated legacy captions; include controlled regression coverage.

* Persist validated engineering cards independently of mass and optics (#1060)

Render constituent under #5493. Fixes #1048. One pending material-engineering-cards descriptor, immutable released migrations, independent assigned density and source conditions. Nine persistence and ten physical-card regressions plus the fresh quick gate pass. Additional native follow-up is reported separately; no full-stack acceptance is implied.

* Render: progressive GPU path-tracing executor (#2067)

Land the unchanged contributor GPU foundation into render under the maintainer policy in #5493. Fresh quick gate 39993339605 passed. Original author and co-author attribution retained; no workflow file changes. Final render-to-main CI and jyviko sign-off remain separate.

Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: asynchronous GPU setup and completed-image panel (#2780)

Integrate the original asynchronous setup, progress and completed-image UI after #2067. Refreshed source passes quick gate 40009901424. Original attribution retained. Final render-to-main full CI and jyviko sign-off remain separate.

Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: studio lighting, clear coat and glass on GPU (#2191)

Integrate the original studio renderer after the GPU executor and async UI foundations, under #5493. Fresh render-target quick run 40011408339 passes. Preserve attribution, bounded resource admission and documented glass/denoising limits. Historical native benchmarks retain their source pins. Full renderer-to-main CI and jyviko sign-off remain separate.

Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>

* GPU path tracing: use compatible Vulkan adapters (#3306)

Land the final Vulkan-only path-executor policy after #2191 under the #5493 constituent route. Fixes #3217. Fresh quick run 40012565783 passed. Preserve adapter/software policy and CPU fallback; no DXC dependency or workflow changes. Full native/hardware acceptance and final jyviko sign-off remain separate.

Co-Authored-By: Christian Svensson <blue@cmd.nu>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(material): initialize optional cards in upstream density regression

Fix the E0063 found by quick run 40015318093 while combining main #4785 with render #1060. The new upstream fixture explicitly has no engineering card. Keep every density/name/default-material assertion and production implementation unchanged.

* feat(render): expose independent anisotropic GGX core (#1694) (#1699)

Extract the original numerical source and analytic tests from published #1698 at 463a3e0d into its existing #1699 constituent. Keep current GPU/shared jobs, format and kernel untouched. Add scoped numerical-contract documentation. Full render library 158 passed, strict scoped Clippy and wasm pass, as do assets/layers/lists/format; source tree pinned in the acceptance record. No runtime consumer or final combined renderer acceptance is claimed.

* Sync current main into render, preserving release and viewport changes (#5705)

* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]

* Viewport: whole-body highlights reuse the model's own triangles (#4554) (#5109)

Hovering or selecting whole bodies -- a browser row, a History group, a body
under the pointer in body-pick mode -- built a CPU copy of every body's
triangles into the highlight scene. On a 40-body 4.3 MB STEP assembly that was
147 bodies, 70 MB of triangle copies and a 519 ms frame.

The model already has the same geometry on the GPU. The highlight now names it
and the renderer draws it again in the highlight colour:

- bodies drawn from a display plan: the model's evaluated plan chunks, with a
  hover/selection tint uniform (the chunks stay resident in the model scene);
- bodies drawn as placements of shared triangles: the same placements of the
  model's shared triangles, coloured per instance;
- bodies the model builds on the CPU: their vertex range in the model scene's
  block.

See-through and faded bodies keep the CPU feedback, which colours them; faces
and edges highlighted on their own are unchanged. The highlight looks the same
(verified side by side).

On the assembly the highlight build drops from 519 ms to 9 ms and no longer
copies any triangles; selecting the whole assembly takes 9-20 ms.

Co-authored-by: Claude Code <noreply@anthropic.com>

* Offer to stand up SolidWorks files instead of turning them silently; add Turn Up Axis to the component menu (#5655)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Sketch on a picked origin plane uses the plane shown after standing an import up (#5659)

* Sketch on a picked origin plane uses the plane shown, also in a moved component

After a STEP file opens its component stays the active one, and standing it up
turns that component a quarter about X. Starting a sketch on XY picked in the
viewport then sent `{"plane": "XY"}`, which names the active component's own
XY: upright after the stand-up. The sketch landed on the world XZ, Look At
turned to face Y and the extrude went along Y.

The viewport and browser only show the design's origin planes, so a picked
origin plane now goes to sketch.create written out as that world plane while
the active component is moved; sketch.create maps it into the component's
frame once. The flip toward the viewer uses the same plane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stand-up axes tests: stand the file up through the offer when one is waiting

#5655 offers to stand up SolidWorks files instead of turning them silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* gpu tests: give the two test frames the new hover and selection tints (#5698)

#5109 added Frame::hover_tint and selection_tint; two test frames that landed alongside it didn't set them, so solidcraft-gpu's tests stopped compiling on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Release 0.21.0-rc.2 (#5691)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>

* GPU: preserve error scopes on cancelled preparation (#5722) (#5723)

* claim: #5722

* fix(gpu): unwind preparation error scopes on cancellation (#5722)

Move fallible CPU HDR packing before GPU scopes and drain the remaining scope stack in reverse order before propagating denoiser/resource allocation failures. Native regressions cover cancellation and allocation-error cleanup plus subsequent scope reuse. Exact candidate Vulkan-required GPU suite: 101 passed, 0 failed, 3 existing ignored; strict GPU all-target Clippy and repository assets/layers/lists/container checks pass. No driver exception, shader transport or test limit is suppressed.

* library: transport PBR and engineering cards without lossy copies (#1086)

* wip(library): reconcile exact rich-catalog replacement semantics (#1062)

Source-only scoped extraction from original 0f801962 / #1086 on the refreshed #1060 card prerequisite. Preserve current asynchronous picker guards and source-ID handles. Scalar v1 and rich-v2-without-PBR clear existing PBR; supplied rich PBR is retained. No new saved concept or duplicate card descriptor. Formatting and source review completed; compile/native acceptance is still pending.

* test(library): preserve explicit downgrade and fallible UI regressions (#1062)

Request scalar v1 explicitly in the card downgrade refusal regression; rich v2 remains the default lossless writer. Propagate fallible harness actions with ? in the shared clicked-catalog helper, preserving all assertions. No production code, limits or ignore changes. Fresh grouped IO/engine/UI tests, scoped strict Clippy and WASM pass on this exact tree.

* Render: defer path-host startup until first use (#5742) (#5743)

* claim: #5742

* fix(render): defer GPU path initialization until first use (#5742)

Retain the selected adapter factory during Design/safe-mode startup; instantiate once on first Render entry, including late viewport installation. Preserve the deferred/preparing/ready/failed state across document changes and retain selected fallback adapter policy. Exact tree passes 32 render-workspace and 1966 full UI tests (0 failures; 7 existing ignored), strict native all-target Clippy, wasm check and repository gates. Three wasm warnings remain in untouched upstream files. Fresh native desktop acceptance is separate and in progress; no whole-renderer/main sign-off implied.

* GPU: adapt dispatches and retire lost devices (#5754) (#5755)

* claim: #5754

* fix(gpu): adapt dispatch size and retire lost path devices (#5754)

Start with a 64-item quantum and adapt bounded work toward 20 ms from observed completion, including denoiser tiles. Never exceed 16384 work items; hardware-specific shader cost means this is not a universal TDR guarantee. Publish device loss/uncaptured failure/poll failure through live executor availability without reinitializing automatically. Source-pinned required-Vulkan GPU suite: 105 passed, zero failed, three existing ignores, including actual device.destroy retirement and session availability. Strict GPU/engine all-target Clippy passes. Broader engine and repository/WASM continuation remains separately recorded. No geometry baseline, CI rule or published format history change.

* fix(render): wrap complete backend diagnostics (#5714) (#5715)

Preserve full backend/job/decoded-image diagnostics in the existing shared panel without ellipsis. The painted-galley regression covers Floating/Compact and light/dark, complete text, wrapping and fixed panel width. Exact-tree Render UI 33/0, strict UI Clippy, WASM and repository gates pass. Real safe-mode window captures, camera restore, CPU export and normal exit are recorded; no backend behavior or unrelated UI policy changes.

* Sync pinned main into render (#5869)

* Viewport: show reference geometry through bodies (Preferences, off by default) (#2697)

A new Graphics preference, "Show reference geometry through bodies", draws
inactive sketch contours, origin and construction planes and their axes over
shaded bodies. It is off by default, so the viewport looks as before: reference
geometry is depth-tested and a section leaves it whole (#1229); Create Sketch
and dialogs picking a plane still draw the planes through the model.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ui-egui tests: offscreen GPU tests start their devices one at a time (#3989) (#3990)

* claim: #3989

* ui-egui tests: offscreen GPU tests start their devices one at a time

Every offscreen test started a wgpu instance of its own, and starting one
makes the Vulkan loader scan its drivers. With several test threads two
tests doing that at once crashed the test process with SIGSEGV inside the
loader (loader_icd_scan). offscreen_device now holds a process-wide lock
while it starts the instance, adapter and device; each test keeps its own
device and the rendering still runs side by side.

* DXF: write and read a Bézier at its own degree (#5269) (#5281)

* claim: #5269

* DXF: write and read a Bézier at its own degree (#5269)

The DXF writer and reader held a spline's degree to 7, the value MAX_BEZIER_DEGREE had before
#1642 raised it to 9. A G4 blend (one Bézier of degree 9) was written as a degree-7 SPLINE over
its first 8 control points, so a flat pattern's cut-out stopped 4.444 mm short in the file.

- seg_shape takes its limit from MAX_BEZIER_DEGREE: a Bézier is written whole, at its own degree.
- read_dxf reads SPLINE degrees 1 to MAX_BEZIER_DEGREE, the degrees a sketch spline takes.
- Tests: every Bézier degree 4 to 9, a degree-9 round trip, the reader's degrees, hostile SPLINE
  degrees and too few control points, the G4 cut-out exported and read back with its areas, and a
  degree-9 SPLINE with 2 to 10 points inserted into a sketch.

Fixes #5269

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Renaming a plane keeps its projections, canvases and hidden state (#4353) (#4354)

* claim: #4353

* Renaming a plane keeps its projections, canvases and hidden state (#4353)

timeline.rename only renamed the feature in other features' references. A sketch's
projection of a construction plane, a canvas placed on it and the hidden list keep it
by name too, so they lost it: the sketch warned that the plane no longer exists, the
canvas left the view and a hidden plane showed again. Document::rename_feature_keys
renames it in sketch links and canvases, as rename_body_keys does for bodies; the
selection follows in the command, and the hidden list in Session::refresh_from, so a
hidden plane stays hidden through undo and redo too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Browser: add Rename and Delete to feature menus (#3839, #4343) (#4342)

* claim: #3839

* fix(ui): add Rename and Delete to browser feature menus

* Browser: a second click on a Document Settings entry closes its window (#4150) (#4152)

* claim: #4150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Browser: a second click on a Document Settings entry closes its window (#4150)

Each entry under Document Settings only opened its window. A second click on
Units closed the menu on the press (the row is outside it) and opened it again
on the release; Precision left Preferences open; Parameters and Design
Properties came back as a fresh copy. Now a click on an entry whose window is
open closes it the way the window's own x does:
- Units: context_menu::toggle_for with the row as the opener, the fix #1522
  made for the workspace button's menu. A right-click still opens it at the
  pointer.
- Precision: prefs::close(app, true), the Cancel/x path that puts back what
  changed since the window opened (now pub(crate)).
- Parameters and Design Properties: the window is dropped, as its x does. A
  Parameters value typed but not applied yet is dropped with it, as today's
  second click already drops it.

Fixes #4150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Preserve expression-positioned sketch fillet corners (#4097) (#4106)

* claim: #4097

* Sketch fillet keeps a corner placed by an expression (#4097)

Filleting a corner whose position comes from parameter expressions deleted
the point and its expressions, so the sketch gained two degrees of freedom
and later parameter changes distorted the outline. keep_corner now also
keeps fixed or expression-positioned points as virtual sharp corners.

* Fix feature editor picks after upstream sketch resizing (#4083) (#4084)

* claim: #4083

* Feature editors refill named edges and faces where they are now (#4083)

After an upstream sketch change, Fillet, Chamfer, Shell and Draft evaluated
correctly, but their editors refilled the originally clicked points and
could then fail with "no edge at ...". Resolve the stored names through the
document's naming helpers first, keep the resolved index, and map the pick
into the displayed body's frame; the point lookup stays as the fallback.

* ui-egui tests: clear the texture deltas of the frames they run (#4039)

Nine tests dropped an egui frame's output with its texture delta still
unapplied (the expression completion editor's frames, the slot preview's
three, the browser's feature rows). epaint asserts against that in debug
builds, so they failed under the dev profile and passed only where debug
assertions are off. They clear the delta as the other tests do; one more
hover test that dropped its output the same way does too.

* Frames: profile library, members along frame sketch lines, and the Frame Member dialog (#4003) (#4004)

* claim: #4003

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Frames: the profile library (#4003)

The first slice of frames and weldments: the cross-sections structural members
will be made of, each SolidCraft's own parametric definition built in code
(no vendor or standard size tables are shipped).

- solidcraft_doc::frame: bar, round bar, rectangular and round tube, angle,
  channel, I-beam and a generic T-slot extrusion as exact line/arc regions,
  with defaults worked out from the profile's size, refusals that name the
  dimension that does not fit, anchors and placement, and exact section
  properties (area, centroid, Ixx/Iyy/Ixy by Green's theorem).
- frame.profile.list, frame.profile.properties (with mass per metre for a
  density) and frame.profile.insert (draws a profile into a sketch, locked by
  default, at a point, anchor and angle).
- Tests against hand-computed values (rectangles, circles, composite
  rectangles by the parallel-axis theorem, fillet spandrels), hostile input,
  extrusion volume, undo; catalog entries in every locale; docs/frames.md;
  ROADMAP M15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Frames: drop the built-in T-slot profile (#4003)

Aluminium extrusions and other vendor sections come from the user's own
sketch (drawn or inserted from the vendor's DXF), so the library keeps the
plain steel shapes only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Frames: members along frame sketch lines (#4003)

frame.member lays one body per straight line of a frame sketch (2D lines or
3D sketch lines), its cross-section square to the line from end to end:

- profile: a built-in profile, or profile_sketch, a sketch whose material
  (nested loops alternating, so two circles make a tube) is the section in
  its own x and y around its origin; profiles picks override it.
- anchor (origin, centre, centroid or a bounding-box corner or edge middle),
  angle (an angle expression, so a parameter), mirror.
- Orientation: the profile's y as near world up as the line allows, x
  square to both; a vertical member's x is world X (frame::member_axes).
- Members follow sketch and parameter edits, undo as one step, save and
  reopen (format: pending/frame-members.rs), and are named after the profile
  (Rect Tube 40x40x3, (2)…), numbering on past names already taken.

Tests against hand-computed volumes and bounding boxes (rectangle, 3D
tower lines, parameter edit, sketch tube profile and picks, anchor/angle/
mirror), refusals, undo/redo and reopen; walk fixtures and goldens; docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Frames: fixes from the adversarial review (#4003)

- Default member lines are what was drawn: projected and other linked curves
  and 3D wires (a face sketch's edges) and lines of no length are left out.
- A profile sketch's material starts from its drawn profiles, as
  solid.extrude's "all" does, so a section sketched on a face works; the
  even-odd test uses outlines and boxes worked out once, capped at 256.
- Member names are pinned to their lines (`names`, by line id; `body_name`
  is the base): deleting or adding a line, or deleting another feature, no
  longer renames other members, and frame.member stores every name it gives.
  body.rename renames a member on its line (also for later member features);
  a copy whose body_names were set still names its members by position.
- Built-in profile dimensions are expressions (ProfileSpec): they follow
  parameters, bare numbers take the design's units, and they are feature
  inputs. frame.profile.properties/insert read them the same way.
- Default root/toe radii shrink to the room a thick section leaves; radii
  given are checked by name; a radius must be 0 or at least 0.01 mm; error
  messages report the wall or flange value given, not twice it.
- A line within 1e-6 rad of vertical orients as vertical, so rounding never
  turns a post a quarter turn.
- A 2D frame sketch in a differently placed component is mapped into the
  member's frame, and member features recompute when placements change.
- Caps before lookups for a design's `lines`; a line listed twice is
  refused; a 3D wire that runs back on itself is not a line.

Regression tests for each, from the review's scenarios.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Frames: the Frame Member dialog on the ribbon (#4003)

- Frame Member (MODEL > Create) opens a dialog: the frame sketch's lines
  picked in the view (or All Lines), a built-in profile and its dimensions or
  a section picked in another sketch, anchor, angle, mirror and name, with a
  live preview. Edit Feature reopens it filled in.
- Lines picked in the view keep their sketch (a SKETCH_LINES input picks
  ExternalSketchCurve): line ids are each sketch's own, so two 3D sketches
  both have a `w2`, and the members went on the newer sketch's lines.
- timeline.redefine takes frame.member. Members keep their names through an
  edit unless it gives them a new body_name; frame.member takes `names`.
- A member whose section is a whole profile sketch keeps it when edited
  (the dialog shows the sketch instead of picking every region, which would
  fill a tube's hole).
- An original frame_member icon, open for review, on the ribbon, timeline
  and browser.
- The angle profile is listed as L-Angle, and web and flange as Web/Flange
  thickness, so they translate apart from the existing Angle, Web and Flange;
  the dialog's labels are in en, ja and zh. en-GB no longer repeats the frame
  command labels it spells the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Rectangular and circular pattern: copies that miss the body are reported (#3042) (#3919)

* claim: #3042

* Warn when cut pattern copies remove no material

A rectangular or circular pattern whose copies miss the body succeeded silently and those copies did nothing. The pattern now reports how many of its cut copies removed no material, and keeps the copies that land.

* Delete in a rolled-back history warns about the features after the marker (#3471) (#3698)

* claim: #3471

* Delete in a rolled-back history warns about the features after the marker (#3471)

With the timeline rolled back, the checks a delete runs first (the
confirmation's "these features will fail", timeline.dependents and
selection.delete's would_fail) compared models evaluated only up to the
marker. Features after it, broken by the delete, were not listed, so their
error marks turned up only once the timeline moved to the end.

newly_broken_in_history evaluates both sides over the whole history when
either has a marker (on copies; the model and document are unchanged) and is
used by the delete's report and by timeline.dependents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* macOS: Fusion navigation preset does not support Shift + two-finger orbit (#3446)

* Macos: keep touchpad/key modifiers true to fusion

* fix: remove obsolete orbit snap reset from trackpad navigation

Upstream removed ViewportState::orbit_snap. The new trackpad orbit path retained one reference, causing E0609 in the PR merge build.

Validation: all seven trackpad tests pass; cargo check -p solidcraft-ui-egui --all-targets passes on the exact failing CI merge 66a2791b with this fix applied; rustfmt and diff checks pass.

Co-Authored-By: GitHub Copilot <noreply@github.com>

* fix: retain Fusion trackpad help in refactored preferences

Preserve upstream's new preferences pages and apply the translated Fusion trackpad tooltip in prefs/navigation.rs.

Validation: cargo build -p solidcraft passed. Tests and full CI were not run, as requested.

Co-Authored-By: GitHub Copilot <noreply@github.com>

* style: format Fusion trackpad preferences tooltip

Apply rustfmt's required wrapping for the relocated tooltip. No behavior changes.

Validation: cargo fmt --all -- --check and git diff --check passed.

Co-Authored-By: GitHub Copilot <noreply@github.com>

---------

Co-authored-by: GitHub Copilot <noreply@github.com>

* 2-point circle takes a typed diameter (#3278) (#3335)

* claim: #3278

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 2-point circle takes a typed diameter (#3278)

After its first point the 2-point circle tool showed no value box, so the
circle could only be sized with the cursor. It now shows a Diameter box,
like the centre circle: the typed value puts the second point that far
along the cursor's direction, and Enter draws the circle.

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>

* Reorder design tabs by dragging them (#3187) (#3188)

* claim: #3187

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reorder design tabs by dragging them (#3187)

A tab dragged along the strip follows the pointer while the others make
room, and lands where it is released; Escape, or releasing down the
window, leaves the order as it was. With the custom title bar a drag that
starts mostly up or down (or on the only tab) still moves the window.
The control channel's ui.documents gains a move action (from, to).

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>

* Renaming a body keeps the evaluated timeline, off the window thread (#2467) (#2468)

* Renaming a body that later features change evaluates nothing (#2467)

Model::rename_body now keeps the results when later features change the
body (a pattern, a chamfer, a join). It still evaluates when a body of
that name comes and goes or isn't the same body throughout, when a later
body's name is worked out from it (X (2)), or when a message quotes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rename box: rename off the window thread, and locked bodies follow a rename (#2467)

The rename box commits body.rename through run_heavy, so a rename that
still evaluates later features (one names the body in its targets)
computes beside the window instead of freezing it. Locked bodies now
follow a body.rename from anywhere (the rename box, the control channel,
scripts), not only the rename box.

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Toolbar: a constraint dragged out of the small box becomes a full button (#5254) (#5456)

* claim: #5254

* Toolbar: a constraint dragged out of the small box becomes a full button (#5254)

The Constrain panel's small box of constraint icons was a fixed list, so
dragging one onto the full-size buttons saved a new order but it was still
drawn in the box. The toolbar settings now remember, per tab, which box
commands were moved out; those are drawn as full buttons, the box closes
up, and dropping one back on a box icon returns it. Reset Toolbar restores
the default box, and settings saved before this load unchanged.

* Sketch offset keeps its source lines free to turn (#4464) (#4466)

* claim: #4464

* Sketch offset keeps its source lines free to turn (#4464)

An offset adds a Parallel between each source line and its offset line.
Since offsets became associative, the offset's points are held during a
solve, so each Parallel pinned its source line to the offset line's
current direction: a closed chain lost its turning freedom (8 degrees of
freedom became 6), and Horizontal or an angle edit on a source line was
refused as conflicting.

The solver and the constructive reading now leave out a Parallel that
ties an offset line to the source it follows while the association lives;
the association already keeps them parallel. After Break the stored
Parallel drives again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Flat Pattern view draws perforations (#4440) (#4441)

* claim: #4440

* Flat Pattern view: draw perforation holes (#4440)

The flat view drew the outline, cut-outs, pockets and bend lines but not
perforation fields, though the flat-pattern data and the DXF export list
their holes. The view now reads the hole centres from the sheet through
SheetBody::perforation_holes, caches them per model state and body, and
draws them as circles in the cut-out style. Holes outside the viewport or
smaller than one pixel are skipped, and the cache is capped at one million
holes.

* "Don't Save" looks like a button in the Save changes prompt (#4380) (#4400)

* claim: #4380

* "Don't Save" looks like a button in the Save changes prompt (#4380)

The prompt's third action used the kit's low-emphasis text style, so it
didn't read as clickable and people thought Cancel was the only way out.
Add an opt-in secondary_left style to the confirm footer (a framed
secondary button from the theme tokens) and use it for Don't Save.

* Save prompt test: clear each frame's texture delta once it is applied

egui 0.36 panics when a TexturesDelta with unapplied deltas is dropped;
the test kept only the last frame's output and dropped the earlier ones.

* 3D mouse test: clear the frame's texture changes, so it passes in debug builds (#4382)

each_frame_hands_the_view_its_motion_through_the_drivers_mapping dropped
the output of a frame it ran (`let _ = ctx.run_ui(...)`). epaint panics
in debug builds when a TexturesDelta is dropped with deltas left ("Dropped
TexturesDelta with 1 unapplied deltas"), so the test failed under a plain
`cargo test` on every platform while the ci profile passed. It now clears
them, as the other UI tests do since #60.

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Spanish (es-ES): catch up with en.json (#4292)

Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>

* Esc ends the navigation bar's Pan, Orbit and Zoom modes (#4226) (#4233)

* claim: #4226

* Esc ends the navigation bar's Pan, Orbit and Zoom modes (#4226)

The nav bar toggled a mode on, but nothing cleared it on Esc, so a left drag
kept panning, orbiting or zooming. Esc now ends an active nav mode first;
the next Esc reaches the tool or dialog as before.

* Keep long Select Other labels inside the popup (#4209) (#4225)

* claim: #4209

* fix(ui): bound Select Other labels and expose full names (#4209)

* Deleting an inserted file removes its components, placements and joints (#4100) (#4113)

* claim: #4100

* Deleting an inserted file removes its components, placements and joints (#4100)

A STEP or mesh file inserted into a design makes a component tree. Deleting
its feature left that tree behind: the components, their occurrences, the
rigid joints that hold them and the bodies' component assignments. A later
body that took an imported body's name was then placed in the stale component.

Document::remove_component_tree removes a component subtree and everything
that refers to it: the features made inside it, its occurrences, the joints
on them with their motion links, joint origins, contact set members, motion
study keys and exploded moves, body component and offset entries, standard
part and link records, and configuration columns. Deleting an inserted
file's feature and component.delete both use it; component.delete now also
removes joints left pointing at a deleted occurrence. The body transfer
refusal moves with it, unchanged.

* Only an inserted file's feature takes its component tree when deleted

Mark the feature a STEP, IGES or mesh insert makes (`inserted`), and remove
the component tree on delete only for those and for a linked design's
feature. Deleting a copied component's independent base deletes just that
feature, as before. Older designs get the mark as they open (pending format
change), except copied components' independent bases.

* Rigid joint saved with the wrong occurrence when two occurrences share a component (#3857) (#3939)

* claim: #3857

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Joint dialog: a body of a further instance picks that occurrence, not the component's first (#3857)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Cancel Sketch: leave a sketch without keeping its changes (#3838) (#3938)

* claim: #3838

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27

* Cancel Sketch: leave a sketch without keeping its changes (#3838)

sketch.cancel (Cancel Sketch, ribbon Exit group and the sketch's
right-click menu) puts the design back as it was when the open sketch
was created or opened for editing: a new sketch is removed with its
dimension parameters, an edited one returns to how it was. It is one
undo step, so Undo goes back into the sketch.

The session remembers the design and the undo history at the point a
sketch is entered. Cancel uses that only while the history still runs
through it (the same steps before, every step since taken in this
sketch); otherwise it refuses and points to Undo, so a stale entry can
never throw away later work.

The icon is the approved close glyph in the sketch accent (alias
sketch_cancel); ja and zh carry the new label.

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>

* 3MF import: name a Bambu Studio project's bodies after its objects (#3858) (#3859)

* claim: #3858 (3MF import: names from a Bambu Studio project)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3MF import: name a Bambu Studio project's bodies after its objects (#3858)

Bambu Studio leaves the name off the root model's objects and keeps it in
Metadata/model_settings.config (<config><object id="N"><metadata key="name">),
so every object of a project came in as Mesh1, Mesh2, ... A build item whose
root object has no name of its own now takes the object's name from those
settings. A name in the model still comes first, and part names, other
settings and objects of other model parts are left alone.

The settings are read only when a body needs a name, within what the model
parts left of the 1 GB budget. Settings that can't be read (malformed, nested
too deeply, too large) never fail the import: the bodies keep the generic
names, with a warning saying why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Viewport: right-click keeps a multi-selection of faces and edges (#3733) (#3788)

* claim: #3733

* Viewport: right-click keeps a multi-selection of faces and edges (#3733)

---------

Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>

* Parameter import and image pickers offer their own file types (#3216) (#3314)

* claim: #3216

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Parameter import and canvas image pickers offer their file types (#3216)

Both opened the picker with no file type of their own, so it listed only
designs, CAD files and meshes (and All files). macOS lets you pick only
the types a filter names: CSV parameters, and PNG or JPEG images for a
canvas, were greyed out. They now ask for csv/json and png/jpg/jpeg,
which the picker lists first (#1801).

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>

* Return to the originating tab after finishing a sketch (#3250) (#3264)

* claim: #3250

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Return to the originating toolbar tab after finishing a sketch (#3250)

A sketch started from SHEET METAL or PLASTIC always ended on the SOLID
tab. Remember the tab that was active when the sketch began
(UiState::sketch_return_tab, not saved) and go back to it when the
sketch ends. Falls back to SOLID when that is not one of the normal
toolbar tabs. A sketch started from SOLID behaves as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Test: no one-element loop in the return-tab test (clippy single_element_loop)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* STEP import: let a cancelled import release the import slot (#3171) (#3189)

* claim: #3171

* Release cancelled file import slots (#3171)

* Dragging a part along its joint no longer highlights what's under the pointer (#5137) (#5140)

* claim: #5137

* Dragging a part along its joint no longer highlights what's under the pointer (#5137)

The viewport's hover pick was skipped during a box selection and camera
moves, but not while a jointed part was dragged, so faces and edges under
the pointer lit up on every frame of the drag. The pick now waits until
the joint drag ends.

* Browser: a body picked in the viewport is opened to and scrolled to (#3005) (#3081)

* claim: #3005

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Browser: a body picked in the viewport is shown in the Browser (#3005)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge main; viewport::select takes a toggle flag now

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>

* Line tool snaps a diameter through the circle centre (#4326) (#4327)

A line drawn across a circle only snapped its end onto the curve, so a
diameter could miss the centre at any angle. Near the centre the line now
locks onto the radial line: on the rim it stays on the circle and the centre
is kept on the line, and on the centre point the end lands there.

* Flatpak: find the host's fonts for CJK text and the font list (#5124) (#5127)

* claim: #5124

* Flatpak: find the host's fonts for CJK text and the font list (#5124)

Inside the Flatpak sandbox the host's fonts are mounted at /run/host/fonts
and /run/host/user-fonts, which no font search looked at. Japanese and
Chinese text fell back to boxes, and the font list missed the host's
families.

- Search both folders for the CJK and Chinese fallbacks and for the
  installed-font list.
- The CJK fallback reads SOLIDCRAFT_FONT_DIRS (a path list, as the font
  list already does), with CRAFT_FONTS_DIR kept as a fallback.

* Crash reports: keep component.activate in the journal (#5079) (#5121)

* claim: #5079

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Crash journal keeps component.activate (#5079)

Activating a component changes only the session's active component and
occurrence path, not the design, so the journal dropped the command and a
crash report's replay built later parts under the previously active
component. The journal now also keeps a command that changes either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Control channel and MCP on by default, with a setting to turn it off (#5561)

The app opens its localhost control channel on port 27878 (or the next free
one) unless Preferences > Assistant turns it off; --control and
SOLIDCRAFT_CONTROL_PORT still win. A current.port discovery file lets
solidcraft-cli mcp find the running app with no arguments.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Tests: moving an occurrence recomputes only the cuts and joins it touches (#4965) (#4982)

* claim: #4965

* Joins and cuts follow only the placements they depend on (#4965)

Tests that turning a joint between parts cuts no hole again, and that a
tool cutting another part cuts again when that part moves relative to it,
while an unrelated part moving keeps the result.

Booleans and Combine now hash only the placements of the bodies they take
as inputs, relative to the frame that evaluates them (#3397), so these
pin that behaviour for joins and cuts with named targets across parts.

* fix(ui): preview shortcut chords and wait for complete modifier input (#1810)

Keep physical modifier events out of the ordinary-key slot, preview held keys immediately, and confirm after release and a one-second settling interval. Preserve legacy control-channel modifier behavior while allowing explicit physical Control/Command input. Add regression coverage, a UI scenario, localized guidance and documentation.

Fixes #1809.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Move: scope box selection to the active component (#4459) (#4527)

* claim: #4459

* fix: scope Move box selection to active component (#4459)

* Delete removes a selected construction axis (#4692) (#4695)

* claim: #4692

* Delete removes a selected construction axis (#4692)

Delete on a construction axis skipped it with "the origin axes can't be
deleted". It now deletes the construction axis feature, like construction
planes; the origin axes X, Y and Z are still refused.

* Agent chat: end the agent's process group where `kill` is busybox' (#4587) (#4588)

* claim: #4587

* Agent chat: end the agent's process group with the system call (#4587)

The group was signalled with a `kill` program: `kill -s TERM -- -<pgid>`.
That works with procps' `kill` and with BSD's, but busybox' `kill` takes no
`--`: it prints its usage, exits 1 and signals nothing. The agent's helpers
(the CLI an adapter wraps, the MCP servers it starts) then stay alive after
the agent is ended.

The group is signalled with the system call instead, so no `kill` program and
no shell are needed and the behaviour is the same everywhere. `nix` (MIT) is
already in the tree through zip, and only on Unix: the browser build does not
take it.

The two tests that cover this now pass where they failed before:
`agent_chat::process::tests::dropping_the_process_ends_its_group` and
`agent_chat::process::tests::stopping_the_agent_leaves_no_helpers_running`.

* Extrude: picking a face, vertex or parallel plane off the profile switches the extent to To Object (#2855)

* Extrude: picking a face, vertex or parallel plane off the profile turns the extent to To Object

With profiles picked, a face off their plane (or curved), a vertex off it, or a
construction plane parallel to it is taken as the To Object target instead of
another profile: the extent switches without the Extent Type choice, and the
distance shows how far the target is. Coplanar faces still join the profiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Extrude: automatic To Object is a Preferences setting, off by default

Preferences > Design > "Extrude to a picked target" (`extrude_auto_to_object`, default
false) turns the automatic To Object on. Off, picks in the Extrude dialog only add
profiles and the distance is not synced to a target, as before this change.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>

* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]

* FreeCAD/Rhino import: decode caps with room for detailed parts (#1783) (#3209)

* claim: #1783

* FreeCAD/Rhino import: decode caps with room for detailed parts (#1783)

cadmpeg decoded FreeCAD and Rhino files under its service profile, whose
million collection items and 2.56e8 work units a detailed part (a sprocket,
a threaded rod, a part with a few hundred faces) runs past: the import was
refused with "resource limit on CollectionItems: BudgetExceeded". Allow
8 million items and 2e9 work units, half of cadmpeg's desktop profile; the
input-size cap stays SolidCraft's STEP limit and every other cap stays at
the service profile's.

decoding_is_capped checks the two raised caps (and that they stay below the
desktop profile's) and that every other cap is still the service one.

Fixes #1783

* A spherical shell revolves into a solid with a void, and cuts and joins with hollow tools work (#451) (#4182)

* Assembly: ground the first part; drag free joint chains (#2804) (#2805)

* assembly(feat): ground first parts and drag free joint chains

Ground the first top-level part when it is created and let face dragging reposition free occurrences or any member of a floating joint chain in one undo step. Preserve exact repeated-placement frames, joint values, modifier selection and Escape cancellation. Keep the accumulated fixture, viewport layout and documentation integration fixes from the existing PR.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(fix): update movable fixtures for first-part grounding

Accept the documented grounded override in component creation, explicitly free movement fixtures, and keep linked clipboard pastes movable even in an empty document. Cover first and reused linked pastes, default insertion grounding, and single-step undo in the existing link tests.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(test): keep the motion-cost crank free

Unground the first component in the document-level joint-face measurement fixture so its motion samples can rotate after first-part grounding. Preserve the rotation and shared-measurement assertions that caught the Linux CI failure.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(test): free the frame placement fixture

Create the frame-member placement fixture ungrounded before moving it above its root sketch. Keep its world-space member assertions while respecting first-part grounding in the frame tests added on main.

Signed-off-by: Karl Von Richter<karl@embeint.com>

---------

Signed-off-by: Karl Von Richter<karl@embeint.com>

* UI: integrate theme radii, borders, palettes and viewport refresh (#5045) (#5046)

* claim: #4922

* UI: let themes own corner radii

* UI: use four-point panel and window radii in SolidCraft 95

* UI: align the 95 radius assertion with four-point panels

* claim: #4953

* UI: let themes own structural and control borders

* UI: single Blueprint outlines and dotted secondary rules

* UI: separate container outlines and add theme-owned 95 bevels

* claim: #5007

* UI: apply theme palettes to icons, workspace tabs and Gruvbox settings

* claim: #5045

* Viewport: invalidate cached geometry colours when the theme palette changes

* UI: preserve legacy theme logo colours with the borderless mark

* Section Analysis: OK with no plane picked turns the section off (#2410) (#2411)

While a section is on, Section Analysis opened with no plane picked now
says "No plane picked: OK turns the section off." and OK runs
inspect.section {"clear": true}. With no section on, OK with no plane
still asks for a plane.

Fixes #2410.

* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]

* Close Change Parameters with Escape (#3372) (#3399)

* claim: #3372

* Fix Escape cancellation in Change Parameters

Consume Escape before parameter editors can commit pending text on focus loss, while preserving autocomplete and shortcut capture priority. Add 11 UI regressions covering draft cancellation, preview cleanup, document and history preservation, and reopening saved values.

* Preserve construction references when axes and points are renamed (#4419) (#4449)

* claim: #4419

* fix: preserve renamed construction references (#4419)

* Browser: route body picks into open command dialogs (#2560) (#4371)

* claim: #2560

* fix(ui): route Browser body picks into active dialogs

* Fix midpoint picks in sketch dimensions (#4872)

Retain MID snaps across both picks and materialize reusable midpoint
constraints for point-to-line and point-to-point dimensions. Let midpoint
pairs choose horizontal or vertical spans with pointer placement before
entering a value.

Cover projected model edges, overlapping geometry, helper reuse,
undo/cancel/reopen, and existing angular and line-circle workflows.

* Sketch slots: provide typed dimension inputs for every variant (#2423) (#4318)

* claim: #2423

* fix(sketch): add typed dimensions to every slot tool (#2423)

* Version 0.21.0 on main (released from rc.2's code) (#5774)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): Windows setup for Claude Desktop (#3794)

* docs(mcp): Windows setup for Claude Desktop (paths, two servers, Store config file)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): say the Store build can keep the config elsewhere, not that the other path fails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]

* MCP: wait out long requests, honest batch outcomes, batch tool aliases (#2901)

Rebased onto main as one commit (was 120d26f7, df488f9a, db9d1073 plus
merges of main).

- A request the app reports as still running (#1618) is waited for on the
  same connection through `control.pending`; past the wait the error says it
  was not cancelled and will still apply (or only write its file), later calls
  are not sent behind it, and the new read-only `pending` tool returns its
  result once it lands.
- A failed batch says what really happened: `rolled_back` only when the
  design is back where the batch started (as #3288 / #3242 on main: the app's
  `rolled_back` is believed; for an older app, every completed step undone;
  only a rollback asked for is reported as one), plus `applied`, `not_run` and
  `failed.still_running`.
- A batch step may name a tool (set_parameter, list_commands,
  describe_command, execute, measure, inspect_design, undo, redo, export, save,
  open, new_design, crash_reports); screenshot, body_topology, batch and
  pending are refused before anything is sent.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: driver choice, guided calibration, button mapping and a picture of the device (#1463) (#3825)

* Add a small test boat to steer around with the 3D mouse

A little hull in its own camera, drawn by the UI itself and not the CAD
viewport, so a 3D mouse can be tried out without opening a design. It
brings the model (generated by assets/models/make_test_boat.py) and the
tests for how the camera moves it.

Nothing uses it yet; Preferences picks it up with the rest of the 3D mouse
work.

* UI kit: keep a choice row from flipping with the scrollbar

A choice row picks segments or a dropdown by whether its labels fit. A page
scrollbar that comes and goes narrows the row by its own width, so the row
could flip back and forth between the two on a page that sits right at the
edge. It is now judged on the whole row and through a small helper,
fits_steadily, that only changes its answer once the miss is wider than the
scrollbar.

Also tests for that, for typed numbers in a slider's value box, and a
test hook to put up the issue-report input barrier by hand.

* 3D mouse: device model, guided calibration, buttons and a picture

The 3D mouse becomes a device we know something about instead of six raw
numbers.

The reader picks between spacenavd and the direct hidraw driver, falls back
from one to the other, and says so when it can't open the device. Guided
calibration asks whether the device is a spring cap or a trackball, adds a
touch step, and uses a settle filter so a ball let go of stops reading as
motion. Buttons get jobs (keys, held modifiers, commands), saved per
device, with defaults for the SpaceMouse Pro.

Preferences gets a picture of the device that moves as it does, next to the
test boat, and the same picture can sit in the 3D view at Left Viewport,
Right Viewport or Left Window, above the floating History card like the
legend and readouts.

These ride together because the crate's calibration types, the Preferences
page, the Linux reader and the button hook all use each other.

* 3D mouse: offer to calibrate an uncalibrated device

When a 3D mouse shows up that has not been calibrated, a card in the 3D
view offers to calibrate it, to swap drivers when the other one would work
better, to close, or to never ask again. It is made once per device, goes
away after 30 seconds in view, and gives way to a status or computing
toast instead of stacking on it.

The card is the shared toast, which now takes buttons: they group by
purpose, wrap inside the card at narrow widths, and do nothing while an
issue report holds CAD input. The picture of the device in the 3D view
keeps clear of the card.

* 3D mouse: translations and attribution

The 3D mouse strings (ui.3d_mouse_*, the calibration steps and the offer
card) in every shipped language, the Japanese catalog note, and the credit
for the test boat model in ATTRIBUTION.md.

This is last because the code before it only looks keys up: it builds and
runs without these, falling back to English until they land.

* 3D mouse: draw its preferences with the settings kit like the rest of Navigation

The 3D mouse section was a folded header with its own layout. It now
uses the same kit as the sections above it: a settings_section card,
settings_form rows with the shared label column, dividers between the
device, axes, picture and buttons groups, Text::Hint for the helper
lines, and settings_control_width for the dropdowns.

The section is open like every other one (the header still folds it),
so the device badge on the folded header is gone; the calibration row
already names the device. Every setting, string and key is unchanged.
Tests that unfolded the section no longer click it, and the hint lines
are looked up as hints instead of notes.

* 3D mouse: show each axis's live tilt in Preferences, like main's axis rows

Each view axis row is main's axis row again: a meter that follows the tilt of the motion driving it, with the dead zone marked, next to Invert, Speed and the "driven by" menu. The meter reads the calibrated travel when the device has a calibration and the raw reading over full scale when it does not, and asks for a frame whenever the reading changes. The helpers are back as they are on main, and the calibration state is a badge by the enable toggle.

* 3D mouse: line its preferences up with the rest of Navigation

- The two checkboxes sit side by side in the same two-column grid as Wheel and trackpad.
- Row labels stop repeating "3D mouse" and use sentence case: Calibration, Driver, Restore defaults, Reset view, Left viewport, Right viewport, Left window.
- Hints line up under the control column instead of the left edge.
- The six view axes are named Pan X, Pan Y, Zoom, Orbit X, Orbit Y and Roll so they fit the name column.
- Buttons are a divider and rows in a form like the other groups, without the bold sub-heading.
- The driver, dead zone, axes, picture and buttons sit behind a collapsed Advanced section, since most people never touch them.

* 3D mouse: tidy the Advanced test for clippy

* Timeline: suppressing a feature keeps later features' body names (#4302) (#4311)

* claim: #4302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Timeline: suppressing a feature keeps later features' body names (#4302)

Bodies numbered by the counter (`BodyN`) take the next free number on every
recompute, so suppressing a feature that made one moved every later counted
body down one (`Body2` became `Body1`) and broke what named it, such as a
Move of `Body2`. timeline.suppress now stores the current names of later
features' counted bodies in their `body_names` first, as body.rename already
does for a renamed counted body (#1109). Engine only: no evaluation or
design-format change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Timeline: a body made while a feature is suppressed keeps its name (#4302)

Suppressing a feature freed its counted name (`Body1`), so a body made
meanwhile took it and became `Body3` once the feature was back. Suppress
now also stores the suppressed feature's own counted names, and the
evaluator adds the names stored on earlier suppressed features to the
state's `reserved_body_names` (the names a rename freed, #4957), which the
`BodyN` counter passes by. They are taken from the design before each
feature is computed and are part of the state digest, so a cached result
is only reused when they match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cadmpeg 0.6.3: FreeCAD and Rhino import in the browser too (#4482)

cadmpeg 0.6.3 is pure Rust (ruzstd instead of the zstd C library, zip 9), so
solidcraft-io depends on it on every target and the wasm build reads .FCStd
and .3dm files like the desktop app. The browser's file pickers offer them.

0.6.3 replaces DecodeOptions::container_only with a decode depth; ask for
Geometry, what its STEP writer reads. zip 8 leaves the tree, and with it the
three cargo-deny advisory exceptions kept for it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Pattern dialogs: Quantity fields get up/down step arrows (#4260) (#4264)

A new kit control, `ui_kit::count_field` / `count_row`, is an expression field for a
whole-number count with stacked step arrows at its right edge. An arrow steps the
evaluated count by one within the field's limits: a plain number is replaced, an
expression keeps its parameters and gains or adjusts a trailing `+ n`. The text
changes as typing changes it, so the live preview follows.

Used by the Quantity rows of the sketch Rectangular (1-500, both directions together
at most 500) and Circular (2-500) patterns, and the solid Rectangular, Circular and
Path patterns (1-1000).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]

* Join: say so when the joined piece doesn't touch the body (#4434) (#4436)

* claim: #4434

* Join: say so when the joined piece doesn't touch the body (#4434)

A Join (extrude, revolve, sweep, loft, pipe, coil, primitives, thicken,
boundary fill, surface network) whose material meets none of its targets
still joins it to the first target as a separate piece. The result stays
as it was; the feature now warns: "the joined piece doesn't touch `Body1`,
so `Body1` is now in 2 separate pieces (New Body makes it a body of its
own)". History marks it and the command reply carries it.

The check compares pieces (outer shells, not voids) of the bodies the
feature changed or absorbed before and after it, so it holds for the
batched multi-profile path too, stays quiet for a join that bridges
pieces or closes a cavity, and leaves Combine (which joins the bodies it
is given) alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Hole: snap to face and round-edge centres, drag placed holes, section in the dialog (#2171) (#2912)

Placing a Single hole put it exactly where the click landed, with nothing to line it up by and no
way to move it but taking it out and picking again; the dialog was a list of size fields.

- Preferences › Design › Create Hole has two settings, both off by default, so a new installation
  places holes as before:
  - Snap to face and edge centres (`hole_snaps`): the planar face under the cursor offers its
    middle and the centres of its round edges (a boss, a rounded corner, a hole already there) as
    marks; a pick near one, or on or beside such an edge, takes that centre and the mark is named.
    Sketch points on the face still come first; Alt or Ctrl held places it where clicked.
  - Move holes by clicking or dragging (`hole_moves`): each placed hole has a handle that drags it
    (snapping when snaps are on) and takes it out when clicked, and a pick elsewhere on its face
    moves it there. Edit Feature shows the handle too, so a hole can be moved later (#1047).
- The Hole dialog draws a section of the hole as its options make it, with each size in a field
  beside the dimension it sets; a hovered or focused field lights its dimension. The layout is a
  new kit element, `ui_kit::figure_rows` (a caller-painted figure beside captioned fields).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Hole: place a Single hole by distances from reference edges (#37) (#2931)

A Single hole can take up to two straight edges of its face as references,
each with the distance of the hole's centre from that edge's line, measured
in the face's plane. `solid.hole` takes them as `references: [{edge, distance}]`;
the edges are found again by their persistent names, else their points, so
the hole follows them when the part changes, and each distance is a
parameter. A bad reference (no edge there, a curved edge, one square to the
face, parallel edges, a negative distance, a centre off the face) is refused.

In the Hole dialog, Reference 1 and 2 follow the face, each with its Distance
row; the canvas shows each distance as a dimension from its edge to the hole,
with a box just past the edge to type it in. Picking an edge measures its
distance (and its box takes the keyboard), typing one moves the hole, and
moving the hole measures them again. Edit Feature picks the edges again with
their distances.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: Karl Von Richter<karl@embeint.com>
Co-authored-by: Frank <58932224+plexi32@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: raykholo <raykholo@users.noreply.github.com>
Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colin.faulkingham@gmail.com>
Co-authored-by: Karl Von Richter <karl@von-richter.net>
Co-authored-by: HiroKenEz <59478073+HiroKenEz@users.noreply.github.com>
Co-authored-by: Peppe Bergqvist <p@bergqvi.st>
Co-authored-by: GitHub Copilot <noreply@github.com>
Co-authored-by: phanoob <lesur.Stephane@wanadoo.fr>
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Kai Evans <nemshilov@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Tyler Chiriboga <tylerchiriboga.apps@gmail.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: CaRLymx <carlymx@gmail.com>
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
Co-authored-by: Loohi <78334654+Loohi@users.noreply.github.com>
Co-authored-by: leValdemar19 <valdemarnord@hotmail.com>
Co-authored-by: AhmetV <mihendiz10@gmail.com>
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
Co-authored-by: rlartot <94693724+rlartot@users.noreply.github.com>
Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Thodoris Tsagkogeorgas <thodoristsagko@gmail.com>
Co-authored-by: ahmetalpcelik <80778880+ahmetalpcelik@users.noreply.github.com>
Co-authored-by: ealitt <ealitt@gmail.com>
Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: Diogo Soares Rodrigues <dsrodrigues20@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Artem Sushchev <Suschev.Artem+github@gmail.com>
Co-authored-by: Nathan Dane <nathandane.dev@gmail.com>
Co-authored-by: Kai Evans <hi@kaievans.co>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: cronos <crono2250@users.noreply.github.com>
Co-authored-by: rbousigues88 <rbousigues@gmail.com>
Co-authored-by: The Nomad <nomad@positron3d.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: Jacques <59315319+Jacquesvdberg92@users.noreply.github.com>

* feat(render): reconcile definition-local visual mapping (#1214) (#1223)

Retain the original mapping implementation, all eight analytic/hostile-input tests and mapping contract byte-for-byte from #1223. Reconcile only module registration and the current roadmap on render. Actual current render library 166/0; strict all-target render Clippy, wasm32 and repository gates pass. No persistence, sampler, new capability, kernel/vendor change or external code is added. Historical mixed-base full-CI failures are not relabelled.

* feat(render): add bounded linear film accumulation (#1696) (#1697)

Extract the independent FilmAccumulator from owned donor 3b892eab5e. Keep the existing job cancellation and caller-owned admission/work contracts. PNG resolution, primary camera coverage and progressive consumer wiring remain separate. Add analytic statistics, atomic refusal, allocation/range boundaries and real 4096/1048576-count tests. No kernel, document schema, workflow or capability change.

* Sync main into render and preserve GPU CLI help (#5976)

* CLI: --help on each subcommand prints its usage and exits 0 (#4538) (#4545)

* claim: #4538

* CLI: --help on each subcommand prints its usage and exits 0 (#4538)

Every command now answers `--help` and `-h` with its own usage on stdout and
exit 0, the way the top level has since #1828. Before, each subcommand
answered differently: `fuzz --help` started a fuzz run with the default seeds
and ran until it was stopped, `recipe --help` took the flag for a file name,
and `snapshot`, `render`, `oracle`, `run`, `eval`, `script` and `crashes`
answered with an error on stderr and exit 1.

The help is read before the command runs, so `--help` never starts work: no
fuzz run, no MCP server, no file read.

`fuzz` also refuses an argument it doesn't take, as `snapshot` already did: a
flag it doesn't know and a stray word are errors, not arguments to ignore
while a hundred seeds run.

The usage lives in one place now: each command's lines are a constant, and the
top-level usage is composed from them, so the two cannot drift apart. The
top-level usage is unchanged, byte for byte.

# Conflicts:
#	apps/solidcraft-cli/src/main.rs

* Renaming a parameter follows it into sheet-metal and plastic rules and canvases (#4420) (#4423)

* claim: #4420

* Renaming a parameter follows it into sheet-metal and plastic rules and canvases (#4420)

Document::rename_param rewrote other parameters, feature inputs, dimensions,
configurations and joints, but not the expressions of sheet metal and plastic rules or
the plane a canvas lies on, so after a rename the sheet metal and plastic features
using them failed with "unknown parameter" and the canvas left the view. It renames
them there now, and param_users lists them (a rule's field, a canvas's input), so a
parameter they use can't be deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Parameters: rules' features in drives and the graph; shared rule field lists (#4420)

A sheet metal or plastic rule that uses a parameter makes the sheet metal (or plastic)
features rebuild when it changes, so parameters.users lists them in drives and
parameters.graph links them. SheetRule::fields/fields_mut and PlasticRule::fields_mut
replace the hand-written copies of the rules' fields in params.rs, eval.rs and
sheet.manage_rules. Suggested by @CamWight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt (#4420)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Mirror and pattern: a pattern or mirror feature as the source (#395) (#3998)

* claim: #395

* Mirror and pattern: a pattern or mirror feature as the source (#395)

A pattern or mirror of features refused another pattern or mirror as a
source ("Pat cannot be patterned or mirrored yet"). Such a source now
stands for what it copied: its own sources are replayed at each of its
transforms moved by each of ours, to any depth up to eight. Copies of
a feature picked directly keep the instance numbers they had, so
existing designs keep their face names; copies made through a nested
pattern or mirror are numbered after them, so one feature reached two
ways never names two copies alike. A pattern or mirror of bodies as
the source is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Expression completion: keep longer names visible when the typed word is a unit (#3663) (#3664)

* claim: #3663

* Expression completion: keep longer names listed when the typed word is a unit (#3663)

Typing `rad` into an expression box showed nothing because `rad` is the radians unit and
a word that is itself a known token closed the popup. The popup now stays open on such a
word. A complete name (`width` with `width_extra` defined) opens it unarmed: the longer
names are listed with no row selected, Enter submits what was typed and Tab moves on; Up
or Down picks a row, after which Enter or Tab complete it as before. A unit means
something only after a number, so a unit typed anywhere else (`rad` on the way to
`radius`) is a prefix like any other: the popup is armed and Enter completes `radius`
instead of submitting one radian.

A complete token is extended only by names that start with it (`in` lists `inner`, not
`rounding`), a typed unit offers no other units (`in` does not list `inch`), and a unit
right after a number (`2 in`, the form every dialog starts its sizes in) opens no popup at
all, so nothing sits over a dialog's OK button.

Fixes #3663

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Pålsson <martin.palsson@outlook.com>
Co-authored-by: Daniel <daniel.owsianski@gmail.com>

* Render: isolate matched GGX visible-normal sampling (#1654)

Independent numerical constituent of #5493. Part of #1654; the CPU reflection consumer remains #1698, so this does not close its image acceptance. Original sampler, matched PDF and analytic/hostile-input tests are preserved from the reviewed donor. 189 render tests, strict Clippy, wasm32 and repository gates pass; fresh quick run 40086624771 passes. No CI configuration, saved-data or kernel change.

* GPU: match index-matched Fresnel in path and viewport (#6036)

Fixes #6035. Preserve actual GPU/f64 material-oracle checks in both translations. Scoped Vulkan, render, Clippy, WASM and fresh quick checks pass. No CI configuration or gate changes.

* feat(render): expose bounded original appearance recipes (#1722)

* claim: resolve the main/render synchronization conflict for #6050

* Settle appearance panel scrolling before targeting Search (#6079)

* claim: #6076

* test(render): settle appearance scroll before resolving Search (#6076)

Expanded starter swatches moved under the scenario click while the existing scroll animation was active. Advance 20 deterministic harness frames before resolving the Search handle. No wall-clock sleep, timeout change, assertion deletion or production change. Original native reproduction fails at the empty-state assertion; all 124 scenarios now pass with one pre-existing ignored adhoc entry.

---------

Signed-off-by: Karl Von Richter<karl@embeint.com>
Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: Christian Svensson <blue@cmd.nu>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jackson Oursland <jacob.oursland@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Frank <58932224+plexi32@users.noreply.github.com>
Co-authored-by: raykholo <raykholo@users.noreply.github.com>
Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com>
Co-authored-by: SS-EN <84559909+SS-EN@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colin.faulkingham@gmail.com>
Co-authored-by: Karl Von Richter <karl@von-richter.net>
Co-authored-by: HiroKenEz <59478073+HiroKenEz@users.noreply.github.com>
Co-authored-by: Peppe Bergqvist <p@bergqvi.st>
Co-authored-by: GitHub Copilot <noreply@github.com>
Co-authored-by: phanoob <lesur.Stephane@wanadoo.fr>
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Kai Evans <nemshilov@gmail.com>
Co-authored-by: Tyler Chiriboga <tylerchiriboga.apps@gmail.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: CaRLymx <carlymx@gmail.com>
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
Co-authored-by: Loohi <78334654+Loohi@users.noreply.github.com>
Co-authored-by: leValdemar19 <valdemarnord@hotmail.com>
Co-authored-by: AhmetV <mihendiz10@gmail.com>
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
Co-authored-by: rlartot <94693724+rlartot@users.noreply.github.com>
Co-authored-by: Thodoris Tsagkogeorgas <thodoristsagko@gmail.com>
Co-authored-by: ahmetalpcelik <80778880+ahmetalpcelik@users.noreply.github.com>
Co-authored-by: ealitt <ealitt@gmail.com>
Co-authored-by: Homero Thompson <vdirienzo@gmail.com>
Co-authored-by: Diogo Soares Rodrigues <dsrodrigues20@gmail.com>
Co-authored-by: Artem Sushchev <Suschev.Artem+github@gmail.com>
Co-authored-by: Nathan Dane <nathandane.dev@gmail.com>
Co-authored-by: Kai Evans <hi@kaievans.co>
Co-authored-by: cronos <crono2250@users.noreply.github.com>
Co-authored-by: rbousigues88 <rbousigues@gmail.com>
Co-authored-by: The Nomad <nomad@positron3d.com>
Co-authored-by: Jacques <59315319+Jacquesvdberg92@users.noreply.github.com>
Co-authored-by: Martin Pålsson <martin.palsson@outlook.com>
Co-authored-by: Daniel <daniel.owsianski@gmail.com>
2026-10-24 14:21:58 +00:00
fugazister a633a67e63 Preferences: choose the window system on Linux (Wayland or X11) (#3083) (#3084)
* claim: #3083

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: choose the window system on Linux in Preferences (#3083)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preferences: a window system set over the control channel is saved for the next start again

The merge kept main's check in ui.prefs, which only looked at the anti-aliasing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preferences: the window system section drops the restart badge its note already states

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:12:38 +00:00
Homero Thompson 5e0e7a4c6a CLI: --help on each subcommand prints its usage and exits 0 (#4538) (#4545)
* claim: #4538

* CLI: --help on each subcommand prints its usage and exits 0 (#4538)

Every command now answers `--help` and `-h` with its own usage on stdout and
exit 0, the way the top level has since #1828. Before, each subcommand
answered differently: `fuzz --help` started a fuzz run with the default seeds
and ran until it was stopped, `recipe --help` took the flag for a file name,
and `snapshot`, `render`, `oracle`, `run`, `eval`, `script` and `crashes`
answered with an error on stderr and exit 1.

The help is read before the command runs, so `--help` never starts work: no
fuzz run, no MCP server, no file read.

`fuzz` also refuses an argument it doesn't take, as `snapshot` already did: a
flag it doesn't know and a stray word are errors, not arguments to ignore
while a hundred seeds run.

The usage lives in one place now: each command's lines are a constant, and the
top-level usage is composed from them, so the two cannot drift apart. The
top-level usage is unchanged, byte for byte.

# Conflicts:
#	apps/solidcraft-cli/src/main.rs
2026-10-24 07:35:25 +00:00
Jackson Oursland 612d9d08dd cadmpeg 0.6.3: FreeCAD and Rhino import in the browser too (#4482)
cadmpeg 0.6.3 is pure Rust (ruzstd instead of the zstd C library, zip 9), so
solidcraft-io depends on it on every target and the wasm build reads .FCStd
and .3dm files like the desktop app. The browser's file pickers offer them.

0.6.3 replaces DecodeOptions::container_only with a decode depth; ask for
Geometry, what its STEP writer reads. zip 8 leaves the tree, and with it the
three cargo-deny advisory exceptions kept for it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 03:47:00 +00:00
The Nomad 1f6a2d4daf 3D mouse: driver choice, guided calibration, button mapping and a picture of the device (#1463) (#3825)
* Add a small test boat to steer around with the 3D mouse

A little hull in its own camera, drawn by the UI itself and not the CAD
viewport, so a 3D mouse can be tried out without opening a design. It
brings the model (generated by assets/models/make_test_boat.py) and the
tests for how the camera moves it.

Nothing uses it yet; Preferences picks it up with the rest of the 3D mouse
work.

* UI kit: keep a choice row from flipping with the scrollbar

A choice row picks segments or a dropdown by whether its labels fit. A page
scrollbar that comes and goes narrows the row by its own width, so the row
could flip back and forth between the two on a page that sits right at the
edge. It is now judged on the whole row and through a small helper,
fits_steadily, that only changes its answer once the miss is wider than the
scrollbar.

Also tests for that, for typed numbers in a slider's value box, and a
test hook to put up the issue-report input barrier by hand.

* 3D mouse: device model, guided calibration, buttons and a picture

The 3D mouse becomes a device we know something about instead of six raw
numbers.

The reader picks between spacenavd and the direct hidraw driver, falls back
from one to the other, and says so when it can't open the device. Guided
calibration asks whether the device is a spring cap or a trackball, adds a
touch step, and uses a settle filter so a ball let go of stops reading as
motion. Buttons get jobs (keys, held modifiers, commands), saved per
device, with defaults for the SpaceMouse Pro.

Preferences gets a picture of the device that moves as it does, next to the
test boat, and the same picture can sit in the 3D view at Left Viewport,
Right Viewport or Left Window, above the floating History card like the
legend and readouts.

These ride together because the crate's calibration types, the Preferences
page, the Linux reader and the button hook all use each other.

* 3D mouse: offer to calibrate an uncalibrated device

When a 3D mouse shows up that has not been calibrated, a card in the 3D
view offers to calibrate it, to swap drivers when the other one would work
better, to close, or to never ask again. It is made once per device, goes
away after 30 seconds in view, and gives way to a status or computing
toast instead of stacking on it.

The card is the shared toast, which now takes buttons: they group by
purpose, wrap inside the card at narrow widths, and do nothing while an
issue report holds CAD input. The picture of the device in the 3D view
keeps clear of the card.

* 3D mouse: translations and attribution

The 3D mouse strings (ui.3d_mouse_*, the calibration steps and the offer
card) in every shipped language, the Japanese catalog note, and the credit
for the test boat model in ATTRIBUTION.md.

This is last because the code before it only looks keys up: it builds and
runs without these, falling back to English until they land.

* 3D mouse: draw its preferences with the settings kit like the rest of Navigation

The 3D mouse section was a folded header with its own layout. It now
uses the same kit as the sections above it: a settings_section card,
settings_form rows with the shared label column, dividers between the
device, axes, picture and buttons groups, Text::Hint for the helper
lines, and settings_control_width for the dropdowns.

The section is open like every other one (the header still folds it),
so the device badge on the folded header is gone; the calibration row
already names the device. Every setting, string and key is unchanged.
Tests that unfolded the section no longer click it, and the hint lines
are looked up as hints instead of notes.

* 3D mouse: show each axis's live tilt in Preferences, like main's axis rows

Each view axis row is main's axis row again: a meter that follows the tilt of the motion driving it, with the dead zone marked, next to Invert, Speed and the "driven by" menu. The meter reads the calibrated travel when the device has a calibration and the raw reading over full scale when it does not, and asks for a frame whenever the reading changes. The helpers are back as they are on main, and the calibration state is a badge by the enable toggle.

* 3D mouse: line its preferences up with the rest of Navigation

- The two checkboxes sit side by side in the same two-column grid as Wheel and trackpad.
- Row labels stop repeating "3D mouse" and use sentence case: Calibration, Driver, Restore defaults, Reset view, Left viewport, Right viewport, Left window.
- Hints line up under the control column instead of the left edge.
- The six view axes are named Pan X, Pan Y, Zoom, Orbit X, Orbit Y and Roll so they fit the name column.
- Buttons are a divider and rows in a form like the other groups, without the bold sub-heading.
- The driver, dead zone, axes, picture and buttons sit behind a collapsed Advanced section, since most people never touch them.

* 3D mouse: tidy the Advanced test for clippy
2026-10-24 03:34:21 +00:00
SS-EN 6c6ffe4802 MCP: wait out long requests, honest batch outcomes, batch tool aliases (#2901)
Rebased onto main as one commit (was 120d26f7, df488f9a, db9d1073 plus
merges of main).

- A request the app reports as still running (#1618) is waited for on the
  same connection through `control.pending`; past the wait the error says it
  was not cancelled and will still apply (or only write its file), later calls
  are not sent behind it, and the new read-only `pending` tool returns its
  result once it lands.
- A failed batch says what really happened: `rolled_back` only when the
  design is back where the batch started (as #3288 / #3242 on main: the app's
  `rolled_back` is believed; for an older app, every completed step undone;
  only a rollback asked for is reported as one), plus `applied`, `not_run` and
  `failed.still_running`.
- A batch step may name a tool (set_parameter, list_commands,
  describe_command, execute, measure, inspect_design, undo, redo, export, save,
  open, new_design, crash_reports); screenshot, body_topology, batch and
  pending are refused before anything is sent.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 02:41:41 +00:00
Karl Von Richter 0dffb206b1 Assembly: ground the first part; drag free joint chains (#2804) (#2805)
* assembly(feat): ground first parts and drag free joint chains

Ground the first top-level part when it is created and let face dragging reposition free occurrences or any member of a floating joint chain in one undo step. Preserve exact repeated-placement frames, joint values, modifier selection and Escape cancellation. Keep the accumulated fixture, viewport layout and documentation integration fixes from the existing PR.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(fix): update movable fixtures for first-part grounding

Accept the documented grounded override in component creation, explicitly free movement fixtures, and keep linked clipboard pastes movable even in an empty document. Cover first and reused linked pastes, default insertion grounding, and single-step undo in the existing link tests.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(test): keep the motion-cost crank free

Unground the first component in the document-level joint-face measurement fixture so its motion samples can rotate after first-part grounding. Preserve the rotation and shared-measurement assertions that caught the Linux CI failure.

Signed-off-by: Karl Von Richter<karl@embeint.com>

* assembly(test): free the frame placement fixture

Create the frame-member placement fixture ungrounded before moving it above its root sketch. Keep its world-space member assertions while respecting first-part grounding in the frame tests added on main.

Signed-off-by: Karl Von Richter<karl@embeint.com>

---------

Signed-off-by: Karl Von Richter<karl@embeint.com>
2026-10-23 23:15:34 +00:00
bherbruck 98c4f335e8 Control channel and MCP on by default, with a setting to turn it off (#5561)
The app opens its localhost control channel on port 27878 (or the next free
one) unless Preferences > Assistant turns it off; --control and
SOLIDCRAFT_CONTROL_PORT still win. A current.port discovery file lets
solidcraft-cli mcp find the running app with no arguments.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:00:35 +00:00
Tyler Chiriboga 945e6261cc 3D mouse test: clear the frame's texture changes, so it passes in debug builds (#4382)
each_frame_hands_the_view_its_motion_through_the_drivers_mapping dropped
the output of a frame it ran (`let _ = ctx.run_ui(...)`). epaint panics
in debug builds when a TexturesDelta is dropped with deltas left ("Dropped
TexturesDelta with 1 unapplied deltas"), so the test failed under a plain
`cargo test` on every platform while the ci profile passed. It now clears
them, as the other UI tests do since #60.

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:30:38 +00:00
Peppe Bergqvist fdde3f9da9 Add macOS native menu bar (#2361) (#4531)
* claim: #2361

Co-Authored-By: GitHub Copilot <noreply@github.com>

* feat: add native macOS menu bar (#2361)

Reuse document actions and unsaved quit handling in native application, File, Edit, Window and Help menus. Replace duplicate header controls only when native installation succeeds, preserving other platforms and the fallback UI.

Co-Authored-By: GitHub Copilot <noreply@github.com>

---------

Co-authored-by: GitHub Copilot <noreply@github.com>
2026-10-23 13:45:31 +00:00
raykholo be87303683 Web: a trackpad swipe pans instead of zooming (#4116) (#4834)
* claim: #4116

* Web: a trackpad swipe pans instead of zooming (#4116)

Browsers deliver trackpad swipes as wheel events without scroll phases, so
the shared navigation treated every vertical swipe as a mouse-wheel zoom.
The web raw-input hook now brackets point-mode swipes (small or horizontal
deltas) with gesture start/end phases, ending after 100 ms idle or before
line/page wheel or pinch input. Wheel notches and pinch still zoom, and
camera input still goes through navigation::handle.
2026-10-23 13:44:20 +00:00
Mateusz Sych e33c371e99 Model cache: thin the checkpoints of a design too big for its file instead of leaving the cache out (#3220) (#4882)
* claim: #3220

* Model cache: keep fewer states of a design too big for its file instead of none (#3220)

A model cache over the container's budget keeps the final state, then the
slowest features' states while the bodies they add fit (a body several
states hold counts once), and is left out only when not even the final
state fits. Opening evaluates just the features whose state the file left
out, each from the state before it, so the model is whole afterwards.

* Model cache: keep each encoded body apart, copy only what a cache stores

Encoding a model too big for its file no longer copies every distinct body
into one buffer before thinning: the payload is copied from the encodings
once its size fits, so saving holds no more than before.

* Model cache tests: canonical runs in the timeline fixture; rustfmt

* Model cache: count the records a thinned cache keeps; always try the final state

Thinning counted only the bodies, so a model of many copies of one part
(records, not bodies, filling the room) shrank its budget to nothing and
saved no cache although its final state fitted. The choice now counts the
records of what it keeps, always keeps the final state, and says what it
counted, so each retry keeps less and the final state alone is always tried.

* docs: a thinned model cache counts its records and always tries the final state

* Model cache test: the slowest feature's state holds bodies 16 to 23

* Model cache: thinned opens with fingerprints that read the state before a feature

Since #3397 a feature's fingerprint reads where the bodies before it lie.
After a state the file left out, adopt takes the saved fingerprint and the
evaluation that works the state out checks it. A result evaluated ahead
is never taken as one the file left out. The thinned round trips include
the placed components' design, whose cut reads those placements.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-23 13:44:11 +00:00
raykholo dad28abecf solidcraft-cli bench-assistant: the standard Assistant model test (#4851) (#4930)
* claim: #4851

* solidcraft-cli bench-assistant: the standard Assistant model test

Runs the eleven v1 tasks of #4851 against a desktop app that is already
running with a control port. Each task starts a new design and a new chat
with All permissions, runs only the prescribed setup, sends the prompt
once and waits for the turn. The result is checked on the design
(document.inspect with measurements): body count, volume within 1%
(0.3% for the fillet), bounding-box size in any orientation, the lowest
corner, the body name, live feature kinds, and changed or unchanged
content; the volume question is checked on the reply. Failures get the
issue's categories from the conversation's items. Prints a table, and
writes the issue's comment template (--markdown) and the raw results
with each conversation (--json).
2026-10-23 13:44:02 +00:00
bherbruck b31fddb98e OpenGL: the viewport scene renders 1x again, like the window (#5605)
#5187 gave the scene its own offscreen target with the app's sample count,
which skipped the OpenGL clamp to 1x from 6dd202d. The scene's count now
goes through effective_msaa_samples as well.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:37:27 -04:00
Jackson Oursland 5962f56f87 Viewport: reuse still pictures and draw scenes into a 1x window pass (#4820) (#5187)
* Viewport: show the picture it drew last when nothing changed (#4820)

A frame redrew the whole scene whenever egui asked for a frame, even when
the camera, scene, highlight and overlays were identical: pointer moves over
empty space and UI animations over a still view paid for the full scene.

The renderer now hashes everything the picture depends on (scene keys,
camera, size, section, images, analysis, accessibility map, occlusion) and
blits the last offscreen picture when the hash matches. The first still
frame filters a supersampled picture down once and later frames copy it as
it is. A frame with the same content costs a blit instead of a scene draw.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Viewport: draw the scene offscreen, into a 1x window pass (#4820)

The scene keeps the app's depth, stencil and MSAA, but egui's window pass now
has one sample and no depth. Whenever the two targets differ, the scene
(product previews too) is drawn offscreen and blitted, so the UI pass no
longer clears and resolves a multisampled window.

Over 8 s of pointer moves on the 450E rotor head, the app's GPU intervals fell
from 1325 ms to 1006 ms and egui_render from 892 ms to 509 ms; idle stays at
0%. A GPU test checks the split path's picture matches the direct one.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* 3D mouse: a repeated report at rest asks for no frame (#4820)

A device may repeat its latest report while the cap rests. Publishing it
again asked egui for a frame although nothing changed. Only a report that
changes the reading now asks for one; while the cap moves, the view asks for
the next frame itself.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 13:42:47 +00:00
William Dunn 17f0d6bc68 Format registry: one table of formats, extensions and options (#5215) (#5217)
* Add the format registry: one table of formats, extensions and options in crates/io.

The hosts' Open-dialog filters and the web accept list derive from it, and
registry tests pin it to the path predicates and the command docs.

* Fix format-registry tests that compared FormatSpec without Eq.

assert_eq on Option<&FormatSpec> does not compile; the none cases
only need is_none, matching the other rows that compare ids.
2026-10-23 13:42:40 +00:00
Mateusz Sych 94279fe059 Web: Ctrl+Tab and Ctrl+Shift+Tab switch browser tabs again (#5227) (#5350)
* claim: #5227

* Web: leave Ctrl+Tab and Ctrl+Shift+Tab to the browser (#5227)

eframe cancels the default action of every Tab keydown on its canvas
(egui moves focus with Tab), so browsers that let a page cancel their
shortcuts, such as Vivaldi, could not switch tabs while SolidCraft had
the keyboard. A capture listener on the window now stops Ctrl+Tab and
Ctrl+Shift+Tab before they reach the canvas, without cancelling them,
so the browser acts on them as usual. Plain Tab and Shift+Tab still
move between the app's fields. Desktop builds are unchanged.

web-api-test checks it in headless Chrome: Ctrl+Tab and Ctrl+Shift+Tab
keydowns are not cancelled, Tab still is.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-23 13:42:27 +00:00
raykholo 92d3a2fde5 GPU: a lost device stops drawing instead of crashing (#4172) (#4229)
* claim: #4172

* GPU: a lost device stops drawing instead of crashing (#4172)

After the driver resets the GPU (Windows TDR), the first queue write to meet the
lost device panicked inside wgpu-core (PendingWrites::activate unwrapping
DeviceError::Lost). Nothing watched the device for its loss.

- egui-wgpu: RenderState::device_lost (DeviceLost) is set by the device's lost
  callback. The painter draws nothing on a lost device, and before a frame's
  uploads it submits nothing (DeviceLost::probe), so a loss the driver reports
  is found by that submission instead of the first write.
- Pictures (thumbnails of saves, ui.render) watch the same flag: on a lost
  device the capturer refuses with GpuError::DeviceLost without touching it,
  and the picture comes from the CPU rasterizer. Headless capturers watch
  their own devices.
- The app, seeing the loss, closes keeping unsaved changes for recovery (as a
  forced quit does) and starts again; the new start says what happened. A
  second loss within ten minutes starts on the software renderer. The loss is
  noted in the crash journal.
2026-10-23 02:19:35 +00:00
Tyler Chiriboga 6f5d0038ab 3D mouse: find devices on Windows without asking every HID device for its strings (#2614) (#3653)
* claim: #2614 (3D mouse: find devices on Windows without asking every HID device for its strings)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: find devices on Windows without asking every HID device for its strings (#2614)

On Windows the 3D mouse reader rescanned every two seconds while no 3D
mouse was plugged in, with hidapi's refresh_devices(). hidapi's Windows
enumeration opens every HID interface and reads its serial number,
manufacturer and product strings: on USB, three control requests to
every keyboard, mouse and touchscreen, forever, on a machine without a
3D mouse. That fits the report of an idle machine whose USB keyboard
drops off and returns and whose cursor jumps.

Windows now asks Plug and Play which present HID interfaces are
multi-axis controllers (hardware ID HID_DEVICE_UP:0001_U:0008), which
sends nothing to any device, and opens only those with open_path().
hidapi's own discovery is turned off before the reader makes its one
context, so making it touches no device either. macOS keeps hidapi's
enumeration (IOKit's registry); Linux is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AGENTS.md: list the Plug and Play scan among the files allowed unsafe code (#2614)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-22 15:44:27 +00:00
Nathan Dane 4b23bb3a03 Implement compact workspace layout (#2029) (#4533)
* Vertical history strip: left of the browser, compact rows, hollow-circle marker, collapsed rail hover

* Vertical history strip: seam handle to expand and collapse names, drag to resize with snap

* Vertical history strip: no empty gap below the transport

* Show the history seam arrow only on hover (#2028)

* claim: #2021

* Redesign the Assistant chat panel with shared UI components

* Show Assistant in the shared floating panel shell

* Extend floating Assistant to viewport height and shift navigation

* Dock Assistant before history and match docking preview bounds

* Align Assistant top margin with floating shell cards

* Extract a shared panel drag grip

* Rebuild responsive history sizing and three-way docking

* Share browser and History collapse hover styling

* Show the History title as soon as its text fits

* Animate stacked browser and History expansion and collapse

* Make panel folding responsive and independent of frame delays

* Let folding panel contents shrink continuously to header height

* Use four explicit History transport actions in every layout

* Resize the shared Browser and History column from either card

* claim: shared compact-mode containers (#2029)

* Centralize layout-aware UI containers and enforce their use

* Keep docked Assistant at the outer edge beside tool sheets

* Use shared surface policy for Assistant tool space

* Let tool sheets drag over shell panels while avoiding Assistant

* Update Parameters drag bounds assertion for shell overlap

* Keep dragged tool sheets below ribbon and workspace selector

* Enforce universal UI containers in mandatory repository checks

* Remove legacy report windows and require shared window composition

* Remove retired UI styling and require shared controls

* claim: compact layout preference (#2029)

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-21 20:52:36 -04:00
DuBearte c54b711345 CLI: --help prints usage to stdout and exits 0 (#1818) (#1828)
* claim: #1818

* claim: #1818

* fix(cli): --help, -h and help print usage to stdout and exit 0 (#1818)

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-21 20:39:30 +00:00
The Nomad f22a026c27 3D mouse: choose which motion drives each axis (#2782)
* 3D mouse calibration: find each axis from a guided recording (#1463)

A recording of the cap at rest and moved along each axis both ways gives which raw axis is which,
which way it runs, where it rests, how much it wanders at rest and how far it goes each way.
Hands aren't exact: on our SpaceMouse Pro, sliding the cap right moved Ry further than X. So each
raw axis is picked by the one-to-one assignment that best explains all six motions together, each
raw axis scored against its own range so a wider one doesn't win through cross-talk. Each
direction is scaled on its own, so a device that goes further one way still reaches full scale
both ways; a direction under a twelfth of the furthest axis counts as not having responded.

A small state machine walks the user through it and moves on by itself once the cap has been held
out for 0.4 s in one go and let go. "Held out" is the least the solver accepts, twice an axis's own
rest wander, so whatever the solver would accept can be recorded and a noisy axis doesn't hold up a
quiet one.

A calibration also carries a dead zone the user sets, a share of full deflection taken off the
bottom of the travel, with full deflection still reading full scale.

Serde support is behind an optional feature, so the crate still needs nothing by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse calibration in Preferences, saved per device (#1463)

Preferences > Navigation gets a 3D mouse calibration row: whether the device being read is
calibrated, Calibrate... and Forget calibration. The window asks for each motion in turn and
saves what it found under the device (hid, web, spacenavd or 3dxware, with vendor and product ids), so
switching devices or turning spacenavd on doesn't apply the wrong mapping. A calibration is saved
for the device it started with, and ends if another one is read instead.

navigate_frame puts the reading through the device's calibration before the driver mapping, so
the per-axis Invert and speed and everything after them are unchanged. While the window is open
the reading goes to it and the view stays put; it closes with Preferences. Restore Defaults on
the Navigation page keeps calibrations, since they describe the device.

The web app hands over the gamepad's readings as they are, so calibration sees where the cap
rests; navigate_frame now leaves a reading inside the dead zone alone after calibration, so a
cap resting just off zero still doesn't keep redrawing. Whether the cap is moving is asked of the
calibrated reading there, in the web page's idle waker and in both wheel filters. Those run before
the frame, so they keep the last frame's calibration with its device and use it only for that
device. On the desktop, an axis with nothing fresh (timed out, or not reported since the device
connected) reads as the device's calibrated rest, so a device resting off zero doesn't drift.

A saved calibration that can't be read or used (a hand edit) is dropped on its own when the
preferences load, with a log line.

The window shows the device's raw readings live and has a dead zone slider, a share of full
travel on top of the measured noise; it's saved with the calibration and adjustable in Preferences
afterwards.

New strings are in all 14 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: choose which motion drives each axis (#1463)

After calibrating, which motion of the cap should drive which view axis is partly a matter of
taste: rolling and twisting swapped, say, or Y and Z. Each axis row in Preferences > Navigation
now has a "driven by" choice next to Invert and speed. Picking a motion another axis uses swaps
the two, so each motion always drives exactly one axis, and Reset axes puts them back.

The choice applies after the device's calibration and before the driver mapping, so Invert and
speed stay with the view axis rather than moving with the motion. It's a preference, not part
of a device's calibration, and stays in force when recalibrating; the calibration window says
so while one is set. A saved assignment that doesn't drive each axis once loads as the default.

New strings are in all 14 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: axis choice review follow-ups (#1463)

- An axis's speed, like its Invert, stays with the view axis when a motion is reassigned; tested.
- Calibrating records the device's own reading, before the axis choice, so a saved swap isn't
  baked into a new calibration and then applied twice; tested through a whole calibration.
- A settings file without the axis choice, or with one of the wrong length, loads with the
  default and keeps every other setting; tested.
- Each axis row says "driven by" in sight before its choice, which otherwise only explained
  itself on hover (no help on a touchscreen or the web).
- "Reset axes" is now "Reset axis assignment", so it doesn't read as forgetting the
  calibration just below it; the "driven by" tip reads more clearly in English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: name axis rows by what they do, and motions without a device in mind (#1463)

The axis rows and the "driven by" choices showed the same names, "Push left / right" and so
on, which read as one thing mapped to itself, and they described a cap that springs back to
centre, which a trackball or a knob that stays put isn't.

Each row is now named for what it does to the view: Pan left / right, Zoom, Pan up / down,
Orbit up / down, Orbit left / right or Roll, or Off. The name comes from the driver mapping
navigation used last frame, so it stays right where the mapping swaps axes (object mode swaps
the last two). Before navigation has run there's no mapping to read, and a row is named for
its own motion instead of a guessed effect. The choices are the device's calibrated motions:
Translation X / Y / Z and Rotation X / Y / Z, under a header row, and the calibration window
names a motion that barely moved the same way. This renames #1814's existing rows, which had
no translations; every new name is in all 14 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: rows never fall back to a motion name (#1463)

Without a driver mapping, a row was named for its own calibrated motion,
which is also what its dropdown lists. It now takes the default mapping's
view axis, so the two sides never share a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: grey out Invert and speed on rows the driver turns off (#1463)

A row the driver profile turns off says so on hover, and its Invert and
speed are greyed out, since they do nothing there. Its motion choice stays
live so a motion can still be swapped out. The column header is shorter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse: unmapped rows say Axis 1-6, and the off-row tip explains the swap (#1463)

Without a driver mapping, a row is now just "Axis n" rather than the
default mapping's view axis, which would be a guess there. The tip on a row
the driver turns off says that a motion swapped into it stops moving the
view, and a test swaps one in and back out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 3D mouse calibration: a device that stops short of rest after a release (#2778)

Through spacenavd, a relative-axis device like the Space Navigator never
reports its return to zero, so the reading stays at its last small report
after every release. The wizard never saw the cap let go, and once
calibrated, the stuck value crept the view.

A reading that stays exactly the same, close to rest, now counts as let go,
and where the cap settled after each step goes into the calibration: such a
device rests at the middle of where it settled, with a dead zone out to the
furthest of them. A device that comes back to rest calibrates as before.

* 3D mouse calibration: a steady hold through spacenavd stays a hold (#2778)

* 3D mouse calibration: stopping short of rest only counts from spacenavd and 3DxWare (#2778)

Recentring on where the cap settled kept the rest stage's wander radius but
not its offset, so a rest extreme could fall outside the dead zone; the
radius now grows by how far the centre moved. A reading that stops changing
close to rest only ends a step from a source that sends nothing at rest, so
HID and web devices calibrate exactly as before.

* 3D mouse calibration: settling off-centre counts on any device (#2778)

A drifting stick settles off its rest after a release just as spacenavd's
stuck reading does, but it keeps reporting, noisily. A step now ends once
the reading stays steady, each raw axis within twice its rest wander (so
exactly still from a silent source), close to rest, on any device. The
dead zone takes that wander on top of the furthest settle point.

* 3D mouse calibration: steadiness by the run's spread, settled on the step's own axis (#2778)

An axis that read flat at rest may dither once off-centre, so steady
readings may spread at least 2 units and a 50th of the furthest any step
went. Steadiness is the spread of the whole run, not distance from its
first reading. Close to rest is judged on the axis that set the step's
peak, since a device's axes may have different ranges, and the settle point
is the middle of the steady run.

* 3D mouse calibration: readings close to rest never count as held (#2778)

* 3D mouse calibration: the dead zone reaches every reading of each release (#2778)

Saving one reading of each release and padding it by the rest wander could
miss a reading seen in the same steady run. Each release now keeps the whole
run, and the dead zone is centred on and reaches everything read at rest
and after the releases.

* 3D mouse calibration: settle for half a second before a step counts as let go (#2778)

A hand held still close to rest is less likely to end a step early; a
device that comes back to rest never waits for it.

* 3D mouse calibration: show the dead zones as a game controller tester would (#2778)

Once calibrated, the window, and Preferences under the Dead zone slider,
show three pads: slide, press and twist, and tilt. Each has a circle for
the full travel, the dead zone shaded inside it with each direction its
own size and the user's dead zone on top, and a dot where the cap is now,
lit once the view moves.

* 3D mouse calibration: settle through cross-talk, per-axis steadiness floor (#2778)

The axis that went furthest in a step needn't be the one asked for:
cross-talk can move another raw axis further. A step now counts as let go
once the furthest axis is at least halfway back and some axis that moved
about as far is within a quarter of its own peak; a hand still holding the
push keeps the furthest axis out. The steadiness floor follows each axis's
own reach, and each stage's steadiness starts with its own readings.

* 3D mouse calibration: steady readings may dither ±2 on any axis (#2778)

* space-mouse: make the settle check's near band match the steadiness spread, test the 4-unit floor

near() measured distance from the settled run's middle against the whole tolerance,
so it accepted twice the band the steadiness check does. Use half. Add a short-range
test where only the .max(4.0) floor lets a +-2 dither settle, and one where dither
the spread check rejects is also not skipped as untouched.

* space-mouse: settle on the middle of the steady run so a lopsided dither isnt movement

near() allows half the steadiness spread around the settled point, but the point was the run mean. A dither that is mostly one value with an occasional far one (10, 10, 10, 14) is steady, yet the far reading sat 3 from the mean and started a hold in the next step. Use the middle of the run range instead.

Adds a lopsided-dither test and a test pinning the documented cross-talk limit ([100,300] then [5,90] stays open).

* Judge an untouched cap by the settled run's spread, not a rounded midpoint

Rounding the midpoint to i16 could leave one end of an odd-width run more
than half the tolerance away, so the cap read as touched while sitting at
the end of the run it settled on. Keep the run itself and ask whether one
more reading keeps its spread within tolerance.

* space-mouse: pin the wider skip band of a narrow settled run and a single-reading settle

* space-mouse: keep a step held while a lesser axis stays out as the larger one relaxes

Settling needed one axis furthest and returned, and one axis about as far and settled, which a single relaxed cross-talk axis satisfied alone. Every axis that moved at least half as far as the furthest now has to be back by RETURNED_SHARE of its own peak too.

* 3D mouse: the calibration tests run in a focused window, as the focus check now requires

* 3D mouse: the axis tests run in a focused window, as the focus check now requires

* 3D mouse: the calibrated-rest test runs in a focused window, as the focus check now requires

* Settle a spacenavd reading that is nearly at rest (#3523)

spacenavd sends a record only when the values change and nothing when the cap springs back, so the last small leftover of a release was held forever and the view kept drifting.

A held reading now reads as rest once every axis is within 150 raw units of the device rest and no record has arrived for 0.5 s. A firmer push stays as long as it lasts, and so does a small value next to a large one. 150 is the smallest round number above the worst leftover in the issue (129 on one axis).

* Choose which 3D mouse driver to read on Linux

When both spacenavd and the device itself (hidraw) are there, Preferences now has a "3D mouse driver" choice: Automatic, spacenavd or Direct. Automatic is what it always was, spacenavd when it runs and the device otherwise, so nobody's setup changes. If the chosen one isn't available the other is read and the log says so once. Each driver keeps its own calibration, as before.

To switch without a restart both reads now wake up when idle and look at the choice: spacenavd with a 250 ms read timeout, hidraw opened non-blocking and polled every 5 ms when it has nothing. A record that arrives is read as before, and one split across a timeout is kept. The reader also publishes which of the two exist, which is what shows the combo box; with only one it says which one is in use.

* Build the 3D mouse row header from existing labels

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-21 11:29:33 -04:00