Compare commits

...

469 Commits

Author SHA1 Message Date
bherbruck 72ad6383ef Release 0.22.1 2026-10-25 10:38:39 -04:00
bherbruck 2cf05388f1 Release 0.22.1: version bump, design format 36 (#6628)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:38:36 -04: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
sascha2101 a1b9ca13e3 Edit Feature: a primitive in a moved component keeps its placement (#2926) (#5989)
* claim: #2926

* Edit Feature: a primitive in a moved component keeps its placement (#2926)

Edit Feature passes a primitive's placement (corner, base, axis, center)
through as saved, in its component's frame, as timeline.inspect reports it.
timeline.redefine read it as a world point and mapped it through the
component's placement, so in a turned component the edit moved the
primitive: in the Hinge Assembly, Leaf B's rod flipped and the revolute
joint put the leaf on the other side.

A placement equal to the saved one now stays as saved; a new one is still
mapped from the world.

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

* Move the #2926 tests up in frames_tests.rs

They sat at the end of the file, where main has since added the #6000
test; moving them avoids the merge conflict.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:00:40 +00:00
Colin Faulkingham 699f526429 Show imported mesh edges in shaded and wireframe views (#5662) (#5740)
* claim: #5662

* Show imported mesh outlines in viewport (#5662)

* test: update mesh regression origin visibility setup
2026-10-25 12:54:31 +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
semmelrinde e5613ebfdf Placing and meshing: no threads started for bodies that are done already (#6406)
Every command in a design with placed components places their bodies
again on up to 16 threads, though nearly all of them are taken as they
were placed before (the same body by the same matrix). Those are now
taken on the calling thread and only the rest are placed on threads.
Warming the meshes (and display plans) of a state likewise starts
threads only for the bodies that have none yet.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:48:00 +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
github-actions[bot] 10da6b9efc Regenerate generated files and fold pending files [skip ci] 2026-10-25 12:27:09 +00:00
Ahmad Khan 5a5a98ee80 Sheet metal: Path Flange, a chain of bends on an existing sheet edge (#4094) (#4096)
* claim: #4094

* Sheet metal: Path Flange, a chain of bends on an existing sheet edge (#4094)

Path Flange (`sheet.path_flange`): sketch an open chain of lines across a sheet edge, pick the
edge and one line; each line becomes a flange as long as the line and each corner a bend, one
feature and one body. The edge flange takes further bends (`then`), each on the far edge of the
one before, and the Flange dialog shows them as rows with a bin each.
2026-10-25 11:33:42 +00:00
Mikalai Kukhta 4e2270517a Release: sign and notarize the macOS app (#5749) (#6016)
* Release: sign and notarize the macOS app with a Developer ID (#5749)

package.sh --sign signs the app, the CLI and the DMG with the hardened runtime and a secure
timestamp, notarizes the DMG and the CLI zip, staples the DMG and checks it with stapler and
spctl. The app carries entitlements.plist: com.apple.security.cs.disable-library-validation
lets the hardened runtime load 3Dconnexion's client framework (crates/threedx-client).

signing.sh --setup imports the certificate into a temporary keychain and writes the API key;
--cleanup removes both. The release workflow builds both architectures first (macos-build, no
secrets), then signs and packages in the join job. Without the APPLE_* secrets (forks, dry runs)
the app keeps its ad-hoc signature and the job summary says so.

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

* macOS release: staple the app before the DMG, retry stapler, document renewal (#5749)

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:15:35 +00:00
Colin Faulkingham 0312808b92 DXF: reject non-BMP text instead of replacing it (#5056) (#5709)
* claim: #5056

* fix: reject unsupported Unicode in DXF text (#5056)

* fix: adapt new DXF callers to fallible writer (#5056)
2026-10-25 11:01:32 +00:00
fkirill 9d45fa6b34 Respect design units across length fields (#3271) (#3315)
* claim: #3271

* fix(ui): use document units across length fields (#3271)

* Format

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

* Adapt drag-extrude and joint tests to document-unit length helpers

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

* fix(ui): preserve blank parameter drafts and cover bounded lengths

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:24:30 +00:00
Frank e4f11c8df6 Preview angled construction planes (#5697)
* Preview planes at an angle

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Plane at Angle: test the preview when an existing plane is edited

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

* ui: include ghost setting in plane preview test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: close angle plane regression test block

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:04:57 +00:00
Jesper We bbc3aa4301 Offset Plane: through a point (#3765)
* claim: Offset Plane through a point

* Offset Plane: through a point instead of at a distance
2026-10-25 06:02:14 +00:00
Jesper We 611d4eb6b8 Crash reports: a repeated panic that ends the app is counted, not reported as an unclean exit (#6051) (#6052)
* claim: #6051

* Crash reports: a repeated panic that ends the app is counted, not reported as an unclean exit (#6051)
2026-10-25 06:00:57 +00:00
Kai Evans c50f5364c6 Selection fields: only the active one looks active (#5256) (#5259)
* Selection fields: only the active one looks active

* Selection fields: a filled field keeps its tinted border where a theme fixes the control border's colour
2026-10-25 05:47:23 +00:00
Kai Evans beb38b44aa Select Other: Shift and Ctrl add the choice to the selection, as a click does (#5258) (#5261)
* Select Other: Shift and Ctrl add the choice to the selection, as a click does

* Select Other: follow origin visibility rename in test

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

* Select Other test: click away from the lines' midpoints, which are picks of their own now

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:47:20 +00:00
Ahmad Khan a49698ee3d Value fields step with the arrow keys and the mouse wheel (#2320) (#2608)
* claim: #2320

* Value fields step with the arrow keys and the mouse wheel (#2320)

Up/Down in a focused value field step a plain number by 1 in the unit it is written in
("20 mm" -> "21 mm", "2,5 mm" -> "3,5 mm"), 10 with Shift and 0.1 with Alt; the wheel over
the focused field does the same, a step a notch, instead of scrolling the panel under it.
Expressions such as `width / 2` are left as typed, and an open completion list keeps the
arrows. Every expression field gets this through `params_dialog::complete`: the value box
beside the geometry in every dialog, the dialog rows, sketch dimensions, the dimension
editor, Drive Joint and the Parameters window.

* The value box beside the geometry stays under the pointer

The box rides on the arrow's tip, so each wheel notch that stepped its value moved it, and
after a notch or two the box slid out from under the pointer and the wheel zoomed the view
instead. While the pointer is over it the box now keeps its place, and it goes back to the tip
when the pointer leaves.

* Value stepping is a preference, off by default

Preferences › General › "Step values with the arrow keys and the wheel" (`step_values`,
also through `ui.prefs`) turns on Up/Down and wheel stepping in value fields, and the value
box staying under the pointer while the wheel steps it. Off, as by default, value fields
behave as before: Up/Down move the text cursor, the wheel scrolls, and the box beside the
geometry follows its arrow.

* Value stepping test: Bend Radius is under Advanced now

* Value stepping test: the preference is on the Design page

* Value stepping test: draw the field straight into the test's frame (#2320)

The shared outer-host rule now covers tests too; the field needs no panel around it.
2026-10-25 05:34:03 +00:00
Ahmad Khan 919f54a23e Sheet metal: bend angles as the included (press-brake) angle, set per rule (#4968) (#6040)
* claim: #4968

* Sheet metal rules: bend angles as the included angle (#4968)

A sheet metal rule's angle_display (bend, the default, or included) sets
how the DXF bend notes read each bend: a 58° bend is B1 UP 122° R1 under
an included rule. The bend table always gives included_angle_deg beside
angle_deg (CSV and JSON). Set in sheet.manage_rules and the Sheet Metal
Rules dialog, listed by sheet.rules; the flange angle stays the bend angle.

* Bend table: the included angle as the CSV's last column, so the columns before it keep their places (#4968)

A test also covers a body whose base flange names its rule, which keeps that rule's notes whichever rule is active, and a bend down.

* Sheet Metal Rules: Angle Display reads Bend or Included, so the choice fits its box (#4968)

The rule editor's row contract lists the new row and the key the dialog sends.

---------

Co-authored-by: begsaquib <saquibbeg4444@gmail.com>
2026-10-25 05:11:13 +00:00
Colin Faulkingham 1d0e371e64 Preserve lid edge references after parameter changes (#5580) (#5658)
* claim: #5580

* fix: follow named lid edges after parameter changes (#5580)

* fix: initialize viewport test frame tints

* Drop duplicate tint fields in gpu view tests (main already has them)

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:04:24 +00:00
Matt Van Horn 43565cd33b Snap drawing to where two sketch curves cross (#4967) (#5342)
While drawing, the pointer snaps to the crossing of two lines, circles or
arcs and keeps the new point on both curves. Shift still prefers a midpoint.
2026-10-25 04:50:20 +00:00
raykholo ab9d9ac50f Manage > Units opens the unit picker (#5907) (#5908)
* claim: #5907

* Fix Manage Units to open the shared unit menu (#5907)

Open the browser's existing unit choices for interactive command starts
instead of executing document.units without its required parameter. Anchor
the menu below toolbar buttons and menu rows. Choices still run the normal
command, while programmatic calls require an explicit unit.

Cover error-free opening, unit choices, button anchoring and dismissal,
the current-unit marker, one-step undo/redo, and programmatic calls.

Fixes #5907
2026-10-25 04:35:33 +00:00
rbousigues88 157c0d68d0 Sketch Dimension: point/circle to circle or arc edge (#4064) (#4065)
* Sketch Dimension: point or circle to the edge of a circle or arc (#4064)

Adds PointCircleDistance and CircleCircleDistance (edge-based, like #1863):
solver, constructive seed, drawing, command wiring, tests, UI scenarios and docs.
Saved designs gain two constraint kinds, in one pending format file.

Fixes #4064.

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

* Sketch Dimension scenario: pick the circle at its top edge (#4064)

The bottom edge sits under the Dimension tool's status hint in the scenario window, so the second click never reached the tool.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 04:29:28 +00:00
raykholo 38d7ea4198 Agent chat: each design tab keeps its own conversation (#5439) (#5612)
* claim: #5439

* Agent chat: each design tab keeps its own conversation (#5439)

* Adapt chat-per-tab changes to the new Pending context and cursor tests

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 04:20:54 +00:00
dudedagger42 7c2b2f1559 Revolve takes a planar body face as its profile (#2747) (#5170)
solid.revolve already revolves a planar body face (`face`, `face_body`), but
the Revolve window's Profile input took sketch profiles only, so a face
couldn't be picked. The input now takes planar faces too, and a picked face
is sent as `face`; sketch profiles win when both are picked.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 04:06:08 +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 7af97e3729 Editing an Extrude or Revolve shows its sketch (#5336) (#5443)
* claim: #5336

* Editing an Extrude or Revolve shows its sketch (#5336)

A finished sketch is hidden once a feature uses it, and it stayed hidden
while that feature's dialog was open for editing, so no other profiles
could be picked. The session now holds the sketch of the feature being
edited; the edit dialog sets it while it is open and clears it however
it closes, and that sketch is drawn without changing stored visibility.
2026-10-25 03:26:36 +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
raykholo dbe7dfa4bc Report a bug: title from the workspace and the reporter's words (#5377) (#5445)
* claim: #5377

* Report a bug: title from the workspace and the reporter's words (#5377)

With no dialog open the report was always labelled "Modeling" and its
summary pre-filled as "unexpected result", so every such report arrived
titled "Modeling: unexpected result" and the fallback to the first line
of "What happened?" never applied.

Label the report from the active workspace (Part, Sketch, Surface, Sheet
Metal, Plastic, Mesh, or Render), leave the summary empty so the title
becomes "<workspace>: <first line of the description>", and keep
"operation failed" when an error was recorded. The summary field now
hints that a short title is optional.

The triage script that asks for an empty in-app report to be pasted
recognises the new titles.
2026-10-25 03:03:08 +00:00
bherbruck 1ceab2ed4c One file per change for new UI strings and attribution rows (#6448)
New UI strings go in assets/locales/pending/<slug>.json and new ATTRIBUTION.md rows in
attribution/pending/<slug>.md. The app build, xtask lists/i18n/assets read them as part of the
catalogs, and the Generated files workflow folds and deletes them on main with
cargo xtask fold-pending.

Refs #6439

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:01:41 +00:00
wckdboy 743accca7a Triangulation: split a crossing boundary segment ourselves, never in spade's assert (#4031) (#5409)
* claim: #4031

* Triangulation: split a crossing boundary segment ourselves, never in spade's assert (#4031)

A trimmed face's boundary segment that crosses a constraint already in the
triangulation went to spade's add_constraint_and_split, which asserts
(cdt.rs:978, `!previous_region.is_empty() || from == final_vertex`) when a
split lands so near another vertex or constraint that the piece up to it is
blocked. The kernel guard caught it, but the whole operation failed and a
crash report was written; a STEP body imported empty or not at all (#4598,
also #4293, #4478, #5068, #5229).

PolyBoundary::add_constraints now adds every segment that can be added, then
splits each blocked one itself: a vertex at each crossing, inserted from the
crossed edge's own vertex, and the pieces between added where they can be. A
segment with crowded or very many crossings, or a face with more than four
blocked segments (a rim that runs back and forth across itself), leaves them
out, as truck did before #1653, so the face meshes slightly off instead of
not at all. Pinched faces (#1648) still split where they cross.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:55:24 +00:00
bherbruck 128692da0c Agent chat: no per-message limit by default (#6349)
* Agent chat: no per-message limit by default

The limit is optional now: an Unlimited checkbox on the AI page (checked by default), a
number field when unchecked. A saved 24, the old default, reads as unlimited. The API-key agent's
24-request and the gate's 400-request caps are gone; Stop still cancels.

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

* Agent chat: add the new messages to the complete Japanese and Chinese catalogs

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:54:13 +00:00
rbousigues88 13b4f6e98c Viewport: a selected edge's highlight no longer shows as a wedge on a grazing face (#3544)
* Viewport: lift selected and hovered edge highlights toward the eye

A selected edge's highlight (3 px core, 6 px halo) is a screen-space band that
was depth-tested at the edge's own depth. A face next to the edge seen at a
grazing angle is pushed back by only about a pixel of its slope, so it buried
part of the band, unevenly along the edge: the highlight showed as a thin
wedge instead of a line.

The band is now drawn like a thin cylinder around the edge: its depth is
pulled toward the eye by half its width plus a pixel, measured on screen so
it holds at every zoom. Plain model edges keep their depth (#1434 still
holds); the part of a highlighted edge the model really hides stays dimmed
through hidden_line.

Not yet compiled or tested.

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

* Viewport: lift an edge highlight by how steeply its faces are seen

The first lift (half the band's width plus a pixel, the same on every edge)
was not tied to the faces beside the edge. Each highlight segment now carries
the normals of the two faces it borders (octahedral, packed with its width in
the line's width slot, read as u32 by vs_line): the band is pulled toward the
eye by its half width times how steeply those faces are seen, so a grazing
face no longer buries it, and an edge seen square to its faces is not lifted
at all.

Test: a_selected_edge_on_a_grazing_face_draws_whole (tilts 60 to 88 degrees,
small and large designs, orthographic and perspective; the band 5 mm behind
a face stays hidden). Capturer::rgba_highlighted draws a highlight scene as
the viewport does, for that test.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:48:34 +00:00
raykholo ac53fc1e6f Viewport: bodies stay visible after switching to perspective and back (#5422) (#5614)
* claim: #5422

* Viewport: bodies stay visible after switching to perspective and back (#5422)
2026-10-25 02:46:59 +00:00
Homero Thompson 8d63a9f4cb Theme: the viewport keeps the previous colours until the camera moves (#5802) (#5835)
* claim: #5802

* fix: repaint on a theme change and key the frame cache on the theme tints (#5802)

theme::apply asks for a repaint, so the new visuals are painted on the next pass instead of waiting for another event to schedule one. The gpu frame cache key now includes the theme's hover and selection tints, so a picture cached under the previous theme's colours is not reused.

* test(ui): settle egui's startup repaints before asserting the theme's repaint (#5802)

* test(ui): name the call site that asks for the theme's repaint (#5802)

* test(ui): apply the theme outside a pass so its repaint request stands alone (#5802)
2026-10-25 02:46:41 +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
raykholo a9fd4b1b05 A dragged command window keeps its place (#5357) (#5444)
* claim: #5357

* A dragged command window keeps its place (#5357)

Command dialogs share one dragged offset per dock side for the session
instead of one per title, and closing a dialog no longer forgets it, so
the next command opens where the last one was left. The offset is still
kept inside the viewport, including after the window shrinks.
2026-10-25 02:38:48 +00:00
amolinari-autotex face5e2d31 Hole: independent tapping depth and recessed drill points (#4486)
* feat(hole): checkpoint independent tapped thread depth

* test(hole): cover independent tapped thread depth boundaries

* fix(doc): bind tapped depth to the useful cylindrical wall

* feat(hole): dimension independent thread depth in the section editor

* fix(hole): preserve custom tapping controls after main integration

* feat: add recessed-hole drill points and order depth dimensions

* fix(hole): match the current preferences and UI kit

* fix(hole): verify reference placement with independent tapping depth
2026-10-25 02:31:24 +00:00
Benny1496 efd0e70f7d fix(oracle): use curvature-weighted centroid moments for case 33 (#5442)
* fix(oracle): use curvature-weighted centroid moments for case 33

* style(oracle): preserve reference JSON numeric formatting

* docs(oracle): remove transient case 33 validation note
2026-10-25 02:30:39 +00:00
SiroArt 3f4c6e4571 Display refinement waits until the camera stands still (#3595) (#5446)
Choosing and swapping in a refinement rebuilds and uploads the whole model
scene. During an orbit of a large assembly that happened about ten times a
second, tens of milliseconds each. It now waits until the camera has stood
still for 200 ms.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 02:30:36 +00:00
rvmzym5xv8-star abc39685b1 Parameters: name the limit an out-of-range number or over-long expression broke (#5906)
Part of #4712.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:30:04 +00:00
Colin Faulkingham f93913b3d3 Sketch: accept curve snaps for Midpoint Line (#5981) (#6041)
* claim: #5981

* fix(sketch): resolve midpoint line snaps (#5981)
2026-10-25 02:29:47 +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
Colin Faulkingham 6494f8fe18 AutoConstrain: dimension construction geometry (#6173) (#6185)
* claim: #6173

* fix(sketch): dimension construction curves in AutoConstrain
2026-10-25 02:29:37 +00:00
selune 1623034973 Render: gate bundled studio-data tests in isolated engine builds (#5493) (#6202)
* claim: post-merge Render review corrections (#5493)

* fix(render): gate bundled studio tests in isolated engine builds (#5493)

Addresses jyviko review comments on #5494: compile the two positive payload-dependent regressions only with bundled-render-library. Keep all assertions and the explicit feature-absent refusals. Reproduced 5 pass / 2 fail before the fix; isolated default, CPU without pack, and CPU with pack tests plus strict scoped Clippy and repository checks pass. No production or CI configuration changes.
2026-10-25 02:29:34 +00:00
NOWN-DEV 74004841b4 Sketch text: a text on a path follows the path in the same evaluation (#6215)
* Test: a text on a path follows the path when a parameter moves it

A construction line placed by a dimension that reads a parameter, and a text laid along it: after
the parameter changes the line has moved and the text has not. It follows one evaluation late.

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

* Sketch text: a text on a path follows the path in the same evaluation

Problem: texts are laid out with the sketch's links, before the sketch is solved. A solve that
moves the curve a text runs along (a path placed by a parameter) left the text where the path had
been before the change, so it followed one evaluation late.

Solution: after a solve that keeps the sketch's orientation, every text that runs along a curve
of its sketch is laid out again along that curve as the solve left it; a text that no longer lays
out adds its warning.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:31 +00:00
NOWN-DEV be08517de2 Construction: a plane on a sheet's flat face follows the face (#6217)
* Test: an offset plane on a sheet flange's face follows the flange

An offset plane on the inner face of an edge flange, picked by body and index as model.faces
lists it. When the base gets wider the flange moves along x and the plane stays where the face
was picked; a sketch on the same face follows it.

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

* Construction: a plane on a sheet's flat face follows the face

Problem: a construction plane on a flat face of a sheet body resolved the stored plane, which is
only where the face was when it was picked. When the sheet changed the plane stayed behind, while
a sketch on the same face follows it by its sheet face name.

Solution: a construction plane on a sheet's flat face resolves that face's plane as it is now, as a
sketch on the face does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:28 +00:00
NOWN-DEV 24ed21998b Configurations: a design keeps up to 2048 rules (#6221)
* Test: a design keeps several hundred configuration rules

An assembly that switches a few hundred parts on and off, by two rules each, needs hundreds of
rules; the saved-rule budget refused more than 128.

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

* Configurations: a design keeps up to 2048 rules

Problem: a configuration table kept at most 128 rules. A rule drives one feature's suppression,
so an assembly that switches a few hundred parts on and off (by two rules each) could not keep
its rules. Copying rules for an independent component copy looked up each new id by scanning all
rules, which a larger table makes slow.

Solution: MAX_RULES is 2048; a copy looks its new ids up in a set of the ids taken. The docs say
the new limit.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:25 +00:00
NOWN-DEV 3592031d5e fix: joints on an imported part's hole or face stay on that part (#6368)
The parts of one STEP assembly import share their face names, and each
part's body stays where the file has it, placed in its component by an
offset. A joint origin on a named face (a hole's centre, a face's
centre, an x reference by face, the end a round origin takes) looked the
name up on every body of the design and compared the pick, in the
joint's component coordinates, with each body as it lies. Another part's
face of that name could lie nearer, and the joint resolved on that
part's face: a pin jointed into one plate's hole landed where the other
plate's hole is relative to its plate.

named_face now looks only at the bodies of the joint's component, each
in its own coordinates, a face the name finds exactly before a piece of
it or a face it was merged into, then the nearest. The design-wide
lookup stays for a face whose body has gone to another component.
named_round, x_axis, pin_end and end_faces take the component, as
face_name_at and round_end_at already do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:16 +00:00
NOWN-DEV b8d9db7440 fix: a face-centre joint origin stays put when a hole in its face moves or goes (#6370)
A joint origin at a face's centre (`joint.origin {face}`, `Snap::FaceCenter`) was the face's
area centroid, holes included. A hole cut into that face, moved or suppressed therefore moved
every part held at the face's centre, though the face's outline stayed where it was: on a
200 x 100 plate with a 20 mm hole, 0.31 mm for each 20 mm the hole moves and 0.62 mm when the
hole, 40 mm off the middle, is suppressed.

On a planar face with holes the origin is now the middle of the area the face's outer boundary
encloses (`outline_centre`: the face's triangle sides that no other triangle of the face runs
back along, corners matched by place, chained into loops in the face's plane; the centroid of
the loop enclosing the most area). A named face (`NamedFace::plane`) and the planar face
nearest a point (`nearest_plane`) both use it. A face without holes, or one whose mesh gives no
clean outline, keeps its centroid as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:14 +00:00
NOWN-DEV 2b620338df perf: solve the joints once per parameter change (#6372)
A parameter change evaluates the model and solves the joints inside the
command (so that it can refuse a value that breaks a sketch), and
`Session::execute` refreshes again after the command and solves every joint a
second time, although nothing has moved since. On 40 small parts jointed
rigidly to a plate's holes (half of the joints lifted by an expression),
changing the lifts' parameter took 0.85 s on main and takes 0.45 s now;
changing one part's thickness, 1.96 s and 1.52 s (CLI, medians of 5 runs).

The session now keeps the design the joints were last solved for, as the
solve left it, and the model that solve read. A refresh that finds the same
model, and a design that differs from the solved one at most in its features,
leaves the joints as they are: the joints read features only through the
model, and a value edit stores the new solutions of the sketches it drove
after its solve, so its design is a new one by the time of the second
refresh. Placements and bounding boxes come out identical.

Test: `a_parameter_change_solves_the_joints_once` counts one solve per change
(two before), checks the jointed part still follows the hole, and that a
captured move of a jointed part is still solved back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:11 +00:00
NOWN-DEV a5b6408587 fix: an edge joint origin on a body placed by an offset lands on that edge (#6374)
An edge joint origin looked its edge and face up in the body's own coordinates with the pick in the component's,
and returned the frame in body coordinates. On a body placed by an offset (an imported part's bodies are) a pick on
the edge where it is shown found no edge ("joint origin: no edge there"), or the origin sat away from its edge by
the offset. Edge origins now take the picks into the body's coordinates through the inverse of its offset and place
the frame back by it, as named faces and round origins already do. A body without an offset is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:08 +00:00
bherbruck 78de687902 CI: duplicate-PR check and a 'Changes a default' line (Refs #6439) (#6442)
* CI: duplicate-PR check and a required 'Changes a default' line (Refs #6439)

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

* PR checks: enforce the defaults line only on PRs opened from 2026-10-25 (Refs #6439)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:28:57 +00:00
bherbruck 2741cd9a8b Section Analysis: edges behind the cut no longer show through the cap (#6328) (#6363)
Since #1768 the section's stencil marking draws every face with FACE_BIAS.
Where a back face seen through the cut meets a front face along an edge (a
pocket's wall and its side wall or floor, a fillet and the face it runs
into), the slope push decided which was nearer beside that edge; a front
face winning left those samples unmarked, the cap skipped them, and the
edge, drawn at its own depth, showed through the hatch. In perspective, and
while orbiting in orthographic too, edges popped in and out from frame to
frame.

A second marking pass draws the back faces only (front faces culled, depth
test <=, no depth write), pulled toward the eye as far as FACE_BIAS pushes
faces back, before the cap. The depth buffer keeps FACE_BIAS, so edges on
visible faces still draw whole (#1434).

Tests in crates/gpu/tests/depth.rs: a pocket 2 mm behind the cut, and a
filleted hub with a bore and a cross hole seen from twelve directions, both
depths, orthographic and perspective. On main the pocket shows 45 px of
edges through the cap in perspective and the hub 25 px over the views; with
this change 0 and at most 2.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:03:31 +00:00
raykholo 86f5500914 Sketch: arcs and circles honour construction when they are created (#5352) (#5440)
* claim: #5352

* Sketch: circles and arcs honour construction (#5352)

sketch.arc.center_point, sketch.arc.three_point, sketch.circle.two_point,
and sketch.circle.three_point accepted construction: true but drew normal
geometry. They now mark the new curve as construction, like lines and
rectangles, and their parameter docs list construction?.
2026-10-25 01:56:44 +00:00
DuBearte 7804f9938a Intersection curves: a point that won't project no longer panics (#5562) (#5812)
* claim: #5562

* Intersection curves: a point that won't project no longer panics (#5562)

A quarter disc at a box's corner revolved 360 deg about the box's edge and
cut (a hemisphere whose pole sits on that edge) panicked in truck at
intersection_curve.rs:133: dividing one of the cut's intersection edges
met a point that projects onto neither surface, and the hinted
projection's fallback called subs, which unwraps that same failed
projection. The guard turned it into a failed cut and a crash report.

Now the leader's point stands in, the division stops there, and the
failure is recorded (truck_geometry take_projection_failure). truck-shapeops'
face division checks it after dividing an edge and gives up, so the
Boolean attempt fails as quickly as the panic made it and the kernel goes
on to its other tries: carrying the approximate points on instead made one
kernel test (equal bores crossing) run for minutes. On this design the cut
still fails with an ordinary error, leaving the box whole; nothing panics.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:23 +00:00
DuBearte ab03aa5bc5 Sketch: explicit planes take a frame (world or component) and the reply says which (#6000) (#6015)
* claim: #6000

* Sketch: explicit planes take a frame, world or component (#6000)

sketch.create and sketch.redefine take frame: "world" (the default,
as before) or "component" for an explicit {origin, x_dir, y_dir}
plane. The reply says which frame the plane was read in, with a note
when a world plane goes into a moved or turned component. frame on a
named plane or a face is refused: those are where they are.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:10 +00:00
semmelrinde 2e3f27f06e Extrude: the automatic operation is decided where the extrude starts (#467) (#6104)
With Start set to From Object (or a start offset), the automatic
operation was still decided from the profile: the engine probed for
material just past the profile plane, and the dialog kept the operation
it had latched before the start changed. A profile on a plate's top,
extruded 14 mm down from the plate's bottom face, came up as Cut instead
of Join. The engine now probes from the extrude's start (and, to an
object, reaches from there), and the dialog decides the operation afresh
when the start changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:04 +00:00
DuBearte bda32419bd Windows: a stuck icacls no longer freezes SolidCraft when it writes the control token (#6145) (#6182)
* claim: #6145

* Windows: a stuck icacls no longer freezes SolidCraft when it writes the control token (#6145)

Writing the control token runs icacls to drop the file's inherited
permissions, and waited for it without a limit. On some computers the
new icacls process stays suspended, so connecting an agent froze the app
for good. It now waits at most 5 s; an icacls still running then is ended
and the token written anyway, into the user's own local app data folder.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:01 +00:00
NOWN-DEV c242c484a8 Construction: the root's constructions are found once the design has a component (#6219)
* Test: a plane through a construction axis follows it in a design with a component

Two offset planes, the axis where they meet and a plane through that axis. Once the design has a
component, a parameter change fails the last plane with "unknown construction `Ax`".

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

* Construction: the root's constructions are found once the design has a component

Problem: once a design has a component, the root's construction features are built in the model
placed in the world. That state carried each component's bodies, sketches and threads, but not
its construction planes, axes and points, so a construction built on another one failed on every
evaluation with "unknown construction".

Solution: the world state carries each component's constructions where the component is placed
(moved by its occurrence's placement, as the bodies are).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:56 +00:00
NOWN-DEV 5d37cb01d5 Configurations: config.rule takes several rules at once and checks the rows once (#6223)
* Test: configuration rules given together are checked with all of them in place

Suppressing a box and the cut on it: the box's rule alone fails the row check (the cut has
nothing left to cut), so the two rules can't be added one at a time. Given together they pass,
in one undo step; a bad rule among them adds none.

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

* Configurations: config.rule takes several rules at once and checks the rows once

Problem: config.rule adds one rule and checks every saved row with it. Rules that only hold
together (suppressing a body and the cut on it) could not be added at all: the first one alone
fails the check. A design with many rules also rebuilt every row once per rule.

Solution: config.rule takes `rules: [...]`: each rule is added or replaced as with `rule`, then
the rows are checked once with all of them in place, in one undo step; a rule that fails
validation adds none. The docs describe it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:54 +00:00
NOWN-DEV 7313fd8684 Joints: an origin takes a face name as long as the face's piece name (#6225)
* Test: joint origins take a long piece name

A face split by a cut whose edge runs in many segments is named after every face around its
piece, several hundred characters. joint.origin refused such a name, as model.faces gives it,
over 512 characters, both for a face centre and as a round end.

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

* Joints: an origin takes a face name as long as the face's piece name

Problem: joint.origin and joint.create took a face name (a face centre, a round end, an x
direction) only up to 512 characters. A face that a cut split is named after the faces around its
piece (`<face>#[<face>,…]`), which reaches several hundred characters when the cut's edge runs in
many segments (a slot with a zigzag edge, a cut of text), so a name model.faces gives was refused.

Solution: the face names a joint origin takes are capped at 16384 characters (MAX_FACE_NAME),
still a bound on hostile input.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:52 +00:00
NOWN-DEV 6f340ade69 Sheet metal: a refolded sheet's faces keep the names of the faces they were (#6268)
* Sheet metal: test that an unfolded sheet's flat face is named after its base at every size

An unfold keeps the base in place and lays the flanges out beside it; the flat
face spanning them and the bend strips is the base's face. It was named after
the unfold whenever few of its sample points fell on the base (the bend strips
are many small triangles, the base two big ones), and that changed with the
sizes: a size change renamed every top and bottom face the refold made of it,
and joints and sketches on them lost their faces (40 of 85 joint ends of a
real part after one change). The test unfolds and refolds a base with three
flanges at two flange heights and wants the flat face and every refolded
panel face named after the base.

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

* Sheet metal: the panel an unfold or refold keeps in place names the face it is part of

A face a sheet feature made is named by where its sample points lie on the
input's faces: in place, else taken back through the folds. Only nine points
per face are sampled, triangle centroids picked by index. An unfolded sheet's
flat face spans the base, the flanges laid out beside it and the bend strips
between them; its bend strips are many small triangles, so most samples fell
on them. Those lie on no panel, and one of them made the fold fallback give
up for the whole face. The face was named after the unfold, and the refold
passed that name to every top and bottom face; which way it went changed with
the part's sizes.

For a sheet feature that moved panels, a face the plain vote leaves unnamed
is now looked at closer (up to 256 points): if a part of it lies in place on
the body's own face before (the panel the feature kept), that face names it.
Else the fold fallback votes over the points that can be taken back, when at
least half can, instead of none when one cannot. Other features, and faces
the plain vote names, are named as before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:49 +00:00
NOWN-DEV e260437846 Names: a pattern copy's piece names match as the original's do (#6270)
* Test: a pattern copy's piece names match as the original's do

A copy's faces carry the instance after the name, after any piece qualifier
(`top#[left,slot:a]@F9.1`). A stored piece name of a copy whose bounding faces have changed since
finds no face of the copy at all (None at the first assertion), where the original's identical
name matches by its stem. On a real part: the joint ends on a patterned copy fall back to their
picks after a size change, the original's resolve by name.

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

* Names: a pattern copy's piece names match as the original's do

Problem: a pattern's copy names its faces after the original's with the instance appended
after any piece qualifier (`top#[left,slot:a]@F9.1`). The piece parser expects a qualified name
to end with its qualifier, so a copy's name was its own stem: when the faces around a piece
changed, a stored name of a copy matched no face of the copy at all, where the original's
identical name still matched by its stem. Joint ends on a patterned part fell back to their picks
after a size change and flipped to the other end of their holes.

Solution: `history_names::matches` takes the pattern instances off the end of both names first
and compares the rest as before, within the same copy only (equal instances; an original's name
never matches a copy's face, nor one copy's another's).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:47 +00:00
NOWN-DEV f54ad0f12f Configurations: a parameter edit runs the suppression rules whose condition reads what changed (#6303)
* Test: a switch parameter alone switches the features its rules suppress

Configuration rules that switch features' suppression from a parameter (drive_suppression when
`on` = 0) ran only on config.activate: a plain parameters.change of the switch left the features
as they were, so a script had to activate a row for every change. Red until the fix.

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

* Configurations: a parameter edit runs the suppression rules whose condition reads what changed

Problem: configuration rules ran only when a row was activated or a configuration edited. A
design that switches features on and off by a parameter (a rule that suppresses a feature when
the parameter is 0) did not follow a parameters.change of that parameter.

Solution: parameters.change (one parameter or several) runs, before the rebuild, the enabled
rules that switch a feature's suppression and whose condition reads a parameter whose value
changed (or a feature switched earlier in the same pass), in their order as activation applies
them, and lists them as `switched`. Driven parameters and restrictions still wait for a
configuration edit or activation; a rule whose condition reads nothing that changed leaves a
hand-set suppression alone; a rule that cannot be evaluated is skipped and never blocks the edit.
One undo step with the edit. docs/configurations.md says so.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:44 +00:00
semmelrinde 43f21e8fec Sketch drag: a live frame stops once its numerical solves have done their share (#6330)
A live drag frame shares one work budget between its held solves, but
only their cluster decomposition was charged. The numerical solve (LM)
ran outside it, so in a larger sketch where the geometry can't follow
the cursor, one frame ran 64 failing held solves of about 150 ms each:
10 s per frame (68 points, 50 curves, 58 constraints, found by
randomised sketch editing). The frame budget now also counts the LM
work of its held solves, without stopping a solve: once a frame's share
is spent, the drag takes no more steps in that frame, keeps what it
reached and asks for the next frame (retry), as the viewport already
handles. The same drag now takes about 0.3 s per frame. Solves outside
live frames are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:42 +00:00
semmelrinde 3e1f1a89f8 Assemblies: owners of bodies looked up once per placement pass, body frames only when picked (#6356)
Placing a design's bodies asked each body's owning component once per
placement, and each answer searched the features for the one that made
the body: placements x bodies x features on every command, twice (the
world state and the frames that map a command's picks into components).
Document::model_body_components answers for all bodies at once, and
world_state_reusing and body_frames_with_source use it. The engine's
frames now work out the per-body table only when a command picks a body
or names one; most commands pick none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:39 +00:00
semmelrinde 82253c6f35 Expressions: remember what an expression names and a constant parameter's value (#6360)
Every evaluation fingerprints every feature by the parameters its
expressions name, and works out every parameter, and most of them (named
feature inputs) are literal values such as "10 mm". In a design built
command by command both were lexed and parsed again on every command.
references() now keeps its answers per thread (bounded), eval_params
keeps its last few results for identical definitions (compared in full),
and a parameter that names no other is evaluated once per expression,
unit and design length unit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:37 +00:00
NOWN-DEV f4d937d8a2 parts: a standard part's sketches are fully defined (#6376)
A standard part (a nut, washer, bolt, screw, pin or bearing) drew its sketches without
constraints, so each was left with free degrees: 3 for a hex, 8 to 12 for a revolved outline.
A check of a design that holds parts listed every one of them as not fully defined. The part
builder now fixes each sketch's points and curves before finishing it: the outline is the
part's size, and a resize builds it again the same way.


Claude-Session: https://claude.ai/code/session_01XhgPofrmFDeLvMhar6Wsqw

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:34 +00:00
semmelrinde 75fb613e25 Names: a new feature's and input's number found without searching the design per number (#6377)
A new feature is named after the first free number of its stem
(Cylinder1, Cylinder2 …), and every number tried searched all features'
names: the thousandth cylinder of a design tried a thousand names against
a thousand features. A new input's dN name likewise wrote out every d1,
d2 … it tried. Both now gather the numbers taken after their stem once
(params::numbers_after, which counts only names written as the number
is) and take the first number not among them. The names are the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:31 +00:00
semmelrinde ce614044a7 Evaluation: a feature's fingerprint text is written only when the feature changed (#6397)
Every evaluation fingerprints every feature by its JSON, so a design
built command by command wrote all of its features out as JSON on every
command. The text is now kept (per thread, by feature id) with the
feature it was written from, and written again only when the feature
differs; the text hashed is the same, so the fingerprints are too.
Imports, whose text depends on their STEP digest, are written each time
as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:29 +00:00
semmelrinde 91549d29fb Inspect: bodies measured on all cores, and once while they stay the same (#6412)
document.inspect with measure (what inspect_design and the
solidcraft://design resource ask for) measured every body one after
the other, and again on every call: on a 1,280-body design each call
took 25 to 60 s. It now measures them on all cores, as
inspect.measure does, and each body keeps its measurement in a cell
shared by its clones (with the body it was measured on, as the outline
box is kept), so a design inspected again measures only the bodies that
changed. Placements taken as before keep their placed body's cell too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:23 +00:00
NOWN-DEV b6a5a9166a perf: a pass over the joints evaluates the design's parameters once (#6418)
Every joint whose offset, angle or values are expressions is worked out again
on each solve, twice per solve (re-expressing the joints, then the check that
leaves out one whose expression no longer works out), and each expression that
names a parameter evaluated every parameter of the design anew
(`Document::eval`). The cost grows with the joints with expressions times the
design's parameters: on 40 parts jointed to a plate, each joint lifted by an
expression and each part carrying 13 parameters (522 in all), a joint solve
took 0.55 s on main and takes 0.41 s now, a change of the lifts' parameter
1.06 s and 0.81 s (CLI, medians of 5 runs).

`joints::Params` keeps the design's parameter values once the first
expression names one, and `joints::expressed` takes it, so one pass evaluates
the parameters once. Values and error messages are those of `Document::eval`.

Test: `kept_parameters_evaluate_as_the_design_does` compares values and
errors with `Document::eval`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:20 +00:00
NOWN-DEV 0063954286 Sketch: a sketch whose links moved far is walked there and keeps the stored sides and turns (#6420)
* Test: a label on a path follows its source far

A rectangle of construction lines placed on three points projected from another sketch, with a
centred text on its far side: moved 270 away by the parameters that place the source, its solve
from the stored sketch failed ("the sketch constraints conflict").

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

* Sketch: a sketch whose links moved far is walked there and keeps the stored sides and turns

Problem: a change upstream moves what a sketch projects (or the points its parameters place)
while its own values stay. The solve from the stored state then either failed ("the sketch
constraints conflict") or landed on the mirror image of a side or turn and reported it solved,
because a solve after a link move checks no orientation when no value changed: a line tangent to
a moving circle hopped a diameter to the circle's other side. And a sketch moved so was stored
only when its own values changed, so every later evaluation solved it from its first stored state
again.

Solution:
- flips_from_stored: the orientations of the stored sketch (unsigned offsets, distance
  triangles, and a line's side of a circle it is tangent to) that a solved sketch turned over,
  each read where the stored sketch has its points.
- solve_after_link_move: the new values solved where the links were stored, then the moved
  points walked to where they are now (predictor-corrector: each step starts from the last two
  solutions carried along their secant; steps halve on a failure or a flip and grow while they
  solve).
- Document::solve_sketch: when a linked or fixed point moved and the jump fails or flips, the
  walk; a flip the walk cannot avoid either is reported as one.
- Model::edited_sketches: a sketch whose linked points a value edit moved is stored as solved, so
  the next evaluation starts where this one ended.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:17 +00:00
Colin Faulkingham 6f6b64d484 Export Current View as DXF downloads in the browser (#3431) (#3601)
* claim: #3431

* fix(engine): use browser clock for DXF view export (#3431)
2026-10-25 01:49:58 +00:00
leValdemar19 52e496d017 Sketch Symmetry: centre a line between two fixed lines (#1796) (#3895)
* claim: #1796

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

* Sketch Symmetry: centre a line between a face's edges (#1796)

Symmetry of two edges of a face (its projected outline, whose sides run
in opposite directions) about a construction line between them was
refused with "neither entity can move". The pairing of the line ends by
mirror image landed on main in #3029; this keeps the tool side and the
tests for #1796:

- The Symmetry tool and a three-line preselection take the only
  construction line or centerline as the symmetry line, whatever the
  pick order (picking it first used to mirror it out past the other
  line). Two points and a line take the line. Otherwise the last pick,
  as before.
- Tests: fixed lines drawn the opposite way, pairs by mirror image (a V,
  end to end along the axis), every pick order, a three-line
  preselection, and a scenario clicking a face's edges.

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>
2026-10-25 01:49:54 +00:00
CamWight d557c09c3b Sketch text: measure a line with the rule that draws it (#4813) (#4815)
* claim: #4813

* Sketch text: measure a line by the rule that draws it (#4813)

`TextMetrics`, made once from a font and a style, parses the face once and gives the scale for a capital height, the
distance between lines, a line's width and the characters the font has no glyph for (in order, each time, including
those its character map sends to `.notdef`). `text_geometry_styled` lays its lines out through it: one rule for
measured and drawn text, no kerning, a missing character still drawn as `?`. Measuring builds no outlines, so a text
too large to draw (`MAX_LINK_ENTITIES`, `MAX_TEXT_CHARS`) can still be measured. Every outline stays bit for bit the
same: a test pins five styled texts, captured before the change. The size and spacing checks are shared, and still
run before the font is looked for; a text whose place or angle is not finite now says so instead of naming its
height.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:49:22 +00:00
raykholo 7046f7f0e7 Windows resize smoke test: wait for a settled window before hit testing (#5523) (#5524)
* claim: #5523

* Windows resize smoke test: wait for a settled window before hit testing

Startup placement can move the window between CheckBorder reading its rect
and the hit tests, so correct answers for the new position failed against
stale coordinates. Wait for two reads 200 ms apart to agree before the first
pass, compare the rect after each pass, and retry the pass (up to three tries)
when it moved. A pass with a stable rect fails exactly as before.

Fixes #5523
2026-10-25 01:49:14 +00:00
Homero Thompson 16286249ed Extrude: a picked region expands across the reference geometry that cuts the drawn profile (#5302) (#5530)
* claim: #5302

* Extrude: a picked profile takes the whole drawn profile (#5302)

A sketch on a solid's side face holds the face's outline as projected
reference geometry, and that outline cuts the drawn profile into pieces
of the sketch's graph. A pick by index or by point resolved against those
pieces, so extruding a rectangle that straddles the face's edge took
only the piece under the cursor.

The pick is now expanded to the drawn profile it belongs to, so it takes
every region of it. The drawn profiles are the disjoint faces of the
drawn graph, so an explicit pick of a drawn hole still fills only that
hole (#5176).

* Extrude: a pick expands across the reference geometry that cuts the drawn profile (#5302)

The expansion crossed every region of the drawn profile, so a projected curve that bounds a region inside the profile (the rim of a counterbore) was crossed too and the cut took more material than intended; now the expansion stops at that boundary. Fixes the regression in cmd::features::prism_local_tests::a_ring_cut_round_a_counterbored_hole_in_a_chamfered_disc, and keeps the behaviour of #5302 and of the #5176 invariant.
2026-10-25 01:48:50 +00:00
rlartot 19e2800472 Sketch on the sides of a thin extrude (#2722) (#5781)
* claim: #2722

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

* Sketch on the sides of a thin extrude (#2722)

A thin wall's sides lie off the sketch curves (half the thickness either
side, say), so no curve was under them: its side faces had no role, no
exact history name, and Create Sketch refused them as "can't keep an
attachment". The roles of a thin wall's sides are now
`wall:<curve>:left|right` (the side of the curve it runs beside, in the
direction the curve is drawn), and its open ends `wall:<curve>:start|end`.
They follow the thickness and side settings and survive a save.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:48:35 +00:00
DuBearte a5f7f5a921 Batch: consecutive parameter edits can rebuild the design once (#5822) (#5839)
* claim: #5822

* Batch: consecutive parameter edits can rebuild the design once (#5822)

An agent's batch of parameter edits, one parameter per step, evaluated the
design once per edit: an edit early in the timeline rebuilds nearly all of
it, and 21 such edits ran for minutes.

engine.batch and the MCP batch tool take coalesce_parameters (default
false). With it, each run of consecutive plain parameter edits
(parameters.change with only a name and an expression, or with changes) is
set as one parameters.change {changes}: one rebuild for the run. Any other
step ends a run, so later steps see the design up to date. Every step
still gets its own result (coalesced: the run's length); the run is one
undo step. A run whose combined edit is refused (a new parameter, which
changes can't make; a bad expression) runs step by step, so a refused edit
is still named by its own step. The app's off-thread evaluation computes
the run the same way. The grouping is solidcraft_engine::batch_params,
shared by both runners.

21 parameter edits on a plate with 21 extruded bosses (in-process MCP,
ci profile): 40.1 s step by step, 3.5 s coalesced, the same volume.

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

* Batch coalescing: say a run skips the states between its edits; test it against step by step (#5822)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:31 +00:00
Frank a402c5e502 Snap points: the centre, middle and ends an edge or face offers, in the engine (#5881) (#5882)
* claim: #5881

* Snap points: the centre, middle and ends an edge or face offers (#5881)

Phase 1 of #5881. `solidcraft_engine::snap::snap_points` gives the points
a picked item offers as picks, from Measure's exact shapes: a round edge
its centre (an arc also its ends and exact middle, joined across pieces
the kernel split it into), a straight or other edge its ends and middle,
a flat face the centre of its area, a cylinder or cone the centres of its
ends, a sphere or torus its centre, a vertex itself. Nothing uses them yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:29 +00:00
JIJINBEI cdde7d62b2 Persistent names read by one parser; four misreadings fixed (#5814) (#5966)
* doc: persistent names read as their structure; joint.inspect splits an edge with a blend face first

`persistent_name::{FaceName, EdgeName}` parse a face or edge name once (blends, inner walls,
copies, pieces, numbered pieces, `~k`) and write it back as the same text, bounded against
hostile input. A suffix after a blend's edge naming a later feature belongs to the blend face.

joint.inspect split an edge at its first `|`, so an edge whose first face is a blend
(`F4:blend:F3:+x|F3:+z#[…]|F5:-y`) came back as a cut-off blend name and a string that is no
face. It now uses `EdgeName::parse`.

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

* doc: persistent names as typed values: features, roles, suffixes, a lexer and errors that say where

`FaceName` is a base (made by a feature in a role, a blend and the edge it rounds, an inner
wall, a body's face by index, other text) and its suffixes (copy, piece, numbered piece).
Roles are typed where the vocabulary is known (top, side:<curve>, +x, face:<k>, …) and kept as
written otherwise. The text form is lexed into tokens and read by the grammar in the module's
doc; a name that doesn't read says where and what was expected.

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

* names: read what made a face, its kind and its feature ids through persistent_name

The places that took a name apart by hand to find the feature that made a face, whether it is
a blend, a copy, a numbered piece or a wall, or to remap feature ids on copy, now ask the typed
name (`made_by`, `split_feature`, `is_blend`, `is_copy`, `is_numbered_piece`, `is_wall`,
`features`, `map`). A name that doesn't read falls back to the old text check.

- An offset of a face that only names a blend inside it (`F2:top#[F5:blend:…]`) is no longer
  taken for a fillet's.
- Copying a component remaps feature ids inside a piece's `#[…]` too.
- A shell's inner wall split by a later cut keeps the qualifier on the wall.
- joint.inspect finds the sketch curve of a copied side (`side:l1@F7.2`).

Finding a face by name (strip_piece, parent_piece) is unchanged: it reads a trailing
qualifier as the whole name's and moves once the text form says where it belongs.

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

* engine: regression tests for press-pull on a wall that only names a fillet and joint.inspect on a copied side

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

* names: an edge holds one face or more; made_by reads only the leading token

An open boundary's edge has one face and an edge several faces meet at has more, so an edge name
is a list of faces (a blend's own edge still takes two at most, as before). `made_by` reads just
the leading `F<n>:`, which every name a feature made starts with, so asking it of every face
each frame stays cheap. Names are read up to 1 MiB: a face engraved with text has piece names
past 4 KiB.

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

* persistent_name: split into lexer, parser, write, text and tests

No code changed; callers' paths stay the same (`pub use text::*`).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:26 +00:00
DuBearte 3f6b9fe89f Timeline: reorder anyway (allow_errors) and report the features the move breaks (#5962) (#5988)
* claim: #5962

* Timeline: reorder anyway (allow_errors) and report the features the move breaks (#5962)

timeline.reorder and timeline.reorder_many refuse a move after which a
feature that worked would fail. Sometimes breaking a later feature is the
point: an agent restructuring a design wants the move made, then the list
of what broke so it can repair those features next.

Both take allow_errors (default false: refused as before). With it the
move is made and the reply lists broken: [{feature, error}] (empty when
nothing broke); one undo puts the order back. timeline.reorder_many also
takes active, as timeline.reorder does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:23 +00:00
Nathalie Schönwetter b4acd83c78 A count from a length says how to get a count (#5901) (#6011)
A pattern count such as (L-2*Post)/210 is a length when the 210 is unit-less, and was refused with only "expected a unit-less number, got a length". The message now says to divide by a length, e.g. floor(L / (210 mm)).

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:48:21 +00:00
semmelrinde f7233c4418 Render: a preview setup with exposure renders and names what it leaves out (#5089) (#6073)
render.settings.set stores exposure_ev and the environment controls, but
render.preflight and render.start refused any setup with them because the
CPU image executor doesn't apply them, so such a setup could never render.
The CPU executor now renders it and the replies list the left-out
controls in ignored_controls.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:20 +00:00
Colin Faulkingham 5573ae06fc Prevent body renames from reusing removed body names (#6042) (#6048)
* claim: #6042

* fix: reject body names retained by Remove (#6042)
2026-10-25 01:48:15 +00:00
raykholo 89a4a96630 Ribbon: an active contextual tab looks selected (#4230) (#4242)
* claim: #4230

* Ribbon: an active contextual tab looks selected (#4230)

Contextual tabs (Sketch while editing, Sheet Metal for a sheet body) chose
their solid accent fill before checking whether they were active, so
selected and unselected looked the same.

The selected contextual tab keeps the solid accent pill. An unselected one
now looks like any unselected tab with its accent as an outline, so it still
marks the context without outshining the tab that is selected. Ordinary tabs
are unchanged.
2026-10-25 01:26:47 +00:00
raykholo 34d1ac0376 A straight 3D sketch line can be picked as an axis (#4363) (#4381)
* claim: #4363

* A straight 3D sketch line can be picked as an axis (#4363)

3D sketch wires were always offered as curved, and axis inputs resolved
only 2D sketch lines, so a straight 3D line couldn't be an axis (Plane at
Angle and others). A single-segment wire is now offered as straight and
resolves to its endpoints in model space; splines are still refused.
2026-10-25 01:26:44 +00:00
Thodoris Tsagkogeorgas 0e9e175e79 Two-sided sketch offset shows both distances in the sketch (#4385) (#4386)
A two-sided offset drew only the Distance dimension. Distance 2 now gets
its own value box and arrow, the other way from Distance.
2026-10-25 01:26:41 +00:00
rlartot f11ad0ce28 Ribbon tooltips: written help for six more tools (#2758) (#4592)
Combine, Loft, Mirror, Circular pattern, Rectangular pattern and Sweep get the
same two-sentence help as the first six (en, fr, ja, zh). Text only.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:26:33 +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
Herperlerpicus b671265faa Viewport: show the running tool's icon beside the pointer (#4869) (#4870)
* Viewport: show the running tool's icon beside the pointer

While an interactive tool or a command's dialog runs, its toolbar icon
rides above and right of the pointer in the viewport, so the next click's
command is visible without looking up at the ribbon. It sits clear of the
system arrow and of the sketch tools' coordinate readout below the
pointer, moves left at the right edge and below the readout at the top.

It shows only where a click reaches the model: not over panels, dialogs,
menus or the navigation bar, and not while the camera moves. The icon is
the command's own (the one the ribbon and the dialog header show), so
there is no new artwork. On by default; "Show tool at cursor" in View
settings (`ui.set {"toolCursor": false}`) turns it off.

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

* Viewport: the tool at the cursor is off by default

As asked in review: the badge starts off, and "Show tool at cursor" in
View settings (or `ui.set {"toolCursor": true}`) turns it on. The test
checks the default and turns it on through the View settings checkbox.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:26:27 +00:00
Reefwalker 832ff941d1 Edit Text: explain failed bold previews immediately (#5004) (#5155)
* claim: #5004

* fix: explain failed sketch text previews immediately (#5004)
2026-10-25 01:26:23 +00:00
raykholo 9869997733 Sketch Create menu: line, rectangle, and other families show their icons (#5244) (#5279)
* claim: #5244

* Sketch Create menu: family rows show their icons

The Line, Rectangle, Circle, Arc, Slot, Polygon, and Spline entries in a
sketch's Create drop-down were drawn as plain text buttons. Draw each as
the shared menu row with its first variant's icon and a submenu arrow,
keeping the same popup handling for its variants.
2026-10-25 01:26:20 +00:00
raykholo 745a24bbc8 Sketch: undo while drawing a spline removes the last point, not the sketch (#5304) (#5441)
* claim: #5304

* Sketch: undo and Backspace remove the last pending spline point (#5304)

While a spline tool has points placed, Cmd/Ctrl+Z and Backspace take back
the last point instead of undoing sketch history or deleting the selection.
Undo reaches sketch history again once no spline points remain.
2026-10-25 01:26:15 +00:00
raykholo 26340882c0 Mirror keeps the preselected feature when the object type changes (#5288) (#5453)
* claim: #5288

* Mirror keeps the preselected feature when the object type changes (#5288)

The pre-selection moves into the objects input when a pattern window
opens, and switching Object Type cleared that input, so switching to
Bodies and back lost the selected feature. The window now keeps the
objects of the type not shown and restores them when it is chosen
again. A plain timeline click on a feature already picked keeps it;
Ctrl+click takes it out.
2026-10-25 01:26:13 +00:00
raykholo 8e2b8f2ace Floating history: the last done feature is no longer drawn as selected (#5415) (#5455)
* claim: #5415

* Floating history: the last done feature is no longer drawn as selected (#5415)

The floating history strip passed the last feature before the marker to
HistoryChip as selected, so it looked selected with nothing selected.
Pass only the real selection, and mark the last done feature with a small
tab-accent dot under its chip instead.
2026-10-25 01:26:10 +00:00
ISHII YUKI 34e75979b2 Agent chat: hand WSL paths to agents started through wsl.exe (#5546) (#5549)
* claim: #5546

* Agent chat: hand WSL paths to agents started through wsl.exe (#5546)

An ACP agent started through wsl.exe was handed the session's working
folder and the solidcraft-app bridge's command as Windows paths, which it
can't use: the Claude adapter refuses the session, and the bridge never
starts. When the agent's program is wsl or wsl.exe, convert both to their
WSL form (C:\Users\... -> /mnt/c/Users/...) before session/new, in the
chat and in the Test button. The bridge still runs on Windows and reaches
the app at 127.0.0.1.
2026-10-25 01:26:05 +00:00
N3MI-DG d5eca6cc78 3D mouse: let the overall speed slider reach the 5x the engine allows (#5704)
The Preferences > Navigation overall speed slider stopped at 3.00x, but
`space_mouse::frame` clamps the applied speed to 0.1..=5.0, so the top of
the range the engine honours could not be reached from the UI. Raise the
slider's ceiling to 5.0x to match that clamp; the step, format and lower
bound are unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-25 01:25:58 +00:00
rlartot 77c77829e3 Preferences: Z up orientation figure is drawn right-handed (#5680) (#5741)
* claim: #5680

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

* Preferences: draw the Z up orientation figure right-handed (#5680)

Z up showed Y pointing toward the viewer on the left, a mirrored
(left-handed) figure. Y now points away on the right, like the axis
gizmo. The axis tips move into orientation_axes and a test checks that
X to Y and Y to Z turn counter-clockwise in both figures.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:55 +00:00
Colin Faulkingham f01ef3c6aa Keep the Parameters window height stable when moved (#5078) (#5752)
* claim: #5078

* Keep the Parameters window height stable when moved (#5078)
2026-10-25 01:25:53 +00:00
rlartot f5a6c8fa51 Link the version number to its release notes (#5488) (#5765)
* claim: #5488

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

* Link the version number in the title bar to its release notes (#5488)

The version at the right of the title bar opens the release page of this
build (v<version>, which exists for stable, candidate and nightly
releases) in the browser; a development build opens the release history.
Its click area stops at the report button, which the long version
strings of candidates and nightlies overlap.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:51 +00:00
rlartot 865575e410 Hem: the handle runs along the hem to its tip (#5828) (#5844)
* claim: #5828

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

* Hem: the handle runs along the hem to its tip (#5828)

The hem's on-canvas handle started on the picked edge and pointed square
to the sheet (#4794 only flipped it), so it neither sat on the hem nor
moved the way the Length grows. Length is measured from the outer face
of the fold to the tip, so the handle now starts on the centre line of
the hem layer at that face and points along the hem toward the tip.

The test builds the hem for both Flip states and checks the handle's
direction, its start on the layer's centre line, and that start plus
Length lands on the built tip. It replaces the arrow-side test of #4836,
whose rule (the arrow points to the side the hem builds on) no longer
applies.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:49 +00:00
HiroKenEz 992c93fe5b Find in Timeline scrolls both timeline layouts to the found feature (#5850) (#5863)
* claim: #5850

* Find in Timeline scrolls both timeline layouts to the found feature (#5850)

Find in Timeline selected the feature but neither the bottom History strip
nor the history on the left scrolled to it. The handler now leaves a one-shot
reveal request (TreeState::timeline_reveal); whichever layout draws its items
next scrolls the found item into the middle of its scroll area and drops it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:44 +00:00
raykholo 0343960bd6 Ignore middle-click clipboard paste during navigation (#5855) (#5924)
* claim: #5855

* Ignore middle-click clipboard paste during navigation (#5855)

Firefox on Linux sends a primary-selection paste event when the middle
button orbits or pans. Paste events on frames where the middle button is
pressed, held, or released no longer reach the design; Ctrl/Cmd+V still
pastes.
2026-10-25 01:25:40 +00:00
DuBearte 1d48707d0c Drive Joints: a Show ghost checkbox (#5905) (#5955)
* claim: #5905

* Drive Joints: a Show ghost checkbox (#5905)

Driving a joint draws the moved parts' old place as a translucent ghost,
which gets in the way when driving joints over and over. Drive Joints has
a Show ghost box under its value rows: on by default (nothing changes),
off draws only the moved parts at their new place. The stopped-short
warning stays. The choice is a preference (drive_joints_ghost), so it is
remembered between uses; it affects Drive Joints only.

The preview's Colors carry the choice (ghost), so toggling it recomputes
the preview (its key hashes the colours) and world_preview leaves the
ghost out.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:38 +00:00
DuBearte 443baa41f0 Design tabs: the tooltip says where the file is saved (#5868) (#5974)
* claim: #5868

* Design tabs: the tooltip says where the file is saved (#5868)

Hovering a design tab showed the design's name, the same text as the tab.
The tooltip now says where the file is saved, with a path in the home
folder shortened to ~/ (home::friendly_path, as the start page does), or
"Not saved yet" for a design never saved. When the tab is too narrow for
its name, the full name comes first, then the place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:36 +00:00
Colin Faulkingham d366b8922a Keep the main sketch tool unchanged when a pinned tool starts (#4023) (#6013)
* claim: #4023

* fix(ui): preserve sketch family choices when starting pinned tools
2026-10-25 01:25:33 +00:00
Colin Faulkingham 692265980b Identify commands beside the animated agent cursor (#622) (#6026)
* claim: #622

* fix(ui): identify commands during agent cursor animation
2026-10-25 01:25:31 +00:00
Colin Faulkingham 28fe5fdb43 Extrude: accept manually selected target planes (#4974) (#6030)
* claim: #4974

* fix(ui): accept manual extrude plane targets (#4974)
2026-10-25 01:25:29 +00:00
semmelrinde 26c9d1c445 View toolbar: the Look At tooltip names its shortcut, Ctrl+8 (#2476) (#6071)
Look At already has a key (Ctrl+8, Cmd+8 on a Mac), but the button's
tooltip didn't say so, as Fit's does (F6).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:24 +00:00
semmelrinde f157f5489f test: the desktop-PATH agent probe waits long enough on a loaded machine (#6072)
desktop_path_can_start_an_agent_and_its_runtime gave the version probe
3 s and the first reply 5 s. Under a full test run on a loaded macOS
machine the probe came back empty (version None, twice in a row), while
the test alone passes in 0.4 s. Both limits are now 20 s; a working
agent still answers at once, so the test takes no longer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:22 +00:00
semmelrinde d1ec8dfd5a Agent chat: a Mammouth AI preset for OpenAI-compatible endpoints (#5815) (#6082)
* Agent chat: a Mammouth AI preset for OpenAI-compatible endpoints (#5815)

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

* Agent chat: the key test names the Mammouth AI URL too (#5815)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:19 +00:00
semmelrinde 08d094db39 Shell: an empty Thickness makes no command, so OK waits for one (#960) (#6085)
With the Thickness box cleared, the dialog sent thickness: "" and the
command refused it ("`thickness` must be a number or an expression"),
shown as a red error with Create Issue. The dialog now makes no command
until a thickness is entered.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:17 +00:00
github-actions[bot] 5fa6361ff2 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-25 02:19:39 +00:00
Colin Faulkingham 1f93b6d39c Joint: use the picked straight edge as the motion axis (#5457) (#6086)
* claim: #5457

* fix: align new straight-edge joint snaps with the edge (#5457)
2026-10-25 01:25:14 +00:00
semmelrinde 0477e0ba4e Shell: the dialog sends the picked face's body (#4555) (#6092)
Where a body's face lies flush with another body's face, the face point
alone could resolve to the other body, and Shell then refused with
"select at least one face to remove". The dialog now passes the body of
the picked faces, as the command already accepts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:12 +00:00
semmelrinde ae7176cc86 Extrude: a face picked under a picked profile is extruded too (#3926) (#6099)
With a sketch profile and a body face picked together, OK ran the
profile's extrude first and then looked the face up by its point. Where
the profile's extrude covered the face, it was gone: "no face of Body1
at face". A new extrude now runs its face picks first; an edit still
rebuilds the profile's extrude first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:09 +00:00
semmelrinde eef9a68809 Dialogs: a missing pick on OK is guidance, without Create Issue (#3292) (#6101)
Plane at Angle's OK with an empty Plane or Axis input showed "select a
plane first" or "pick an axis" as a red error with a Create Issue
button, and people filed it as a bug (#3292, #3773). A refusal that only
names what to pick is now shown as a plain hint, with no issue button,
in every dialog (Sketch Modify dialogs already hid it).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:06 +00:00
Colin Faulkingham f06875128b Preserve the history marker when switching sketch editors (#5435) (#6103)
* claim: #5435

* fix(ui): preserve history when switching sketch editors
2026-10-25 01:25:04 +00:00
semmelrinde 9d226b3535 Sheet metal DXF: options to leave out the bend lines and to use one layer (#4314) (#6130)
sheet.export_dxf always wrote the dashed bend lines (BEND_UP,
BEND_DOWN) and split the flat pattern over a layer for each role. A
cutter that cuts every line it is given cut the bend lines too.
bend_lines: false now leaves them out, and layers: single writes every
entity on layer 0. The defaults keep today's output.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:02 +00:00
Colin Faulkingham 7a1f903216 Close the File menu with Escape (#6126) (#6132)
* claim: #6126

* fix: close File menu on Escape (#6126)
2026-10-25 01:25:00 +00:00
NOWN-DEV b88673f366 Model cache: each sheet body is written once, not with every state (#6152)
* Test: a saved model keeps each sheet body once

A sheet-metal design wrote its sheet bodies again with every later feature's state in the saved
model: each feature that leaves the sheet alone (a box beside it) adds more than the whole sheet
to the records. A design of many features after its sheet metal passed the cache's 32 MiB
manifest limit that way and kept only some of its states (#3220). The test also opens the saved
file and wants the sheets taken from it as evaluated.

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

* Model cache: each sheet body is written once, not with every state

Problem: a saved model writes every feature's state, and each state wrote its sheet bodies in
full; most features leave the sheets as they were, so a sheet-metal design of many features
repeated them over and over (a 280-feature part: 30.5 MB of manifest, 29.6 MB of it 69 distinct
sheet records written 243 times). Past the manifest's 32 MiB limit the save thinned the cache
(#3220) and opening evaluated the left-out features again, although the distinct data was small.

Solution: the records keep a list of sheet bodies, each written once; a state names its sheets by
their place in it, as it already does for bodies and sketches. A thinned model keeps the whole
list and counts it once against its budget; the reader counts sheets with the bodies and sketches
a state holds; the cache format is now /4 (an older cache is a miss).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:57 +00:00
DuBearte 67adf5abb5 Preferences: the theme strip shows arrows where more themes are (#6161) (#6167)
* claim: #6161

* Preferences: the theme strip shows arrows where more themes are (#6161)

The strip scrolls sideways with a hidden scrollbar, so nothing showed that
more themes were past its end. An end with themes past it now shows a
chevron button over the tiles there; a click scrolls most of a page.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:54 +00:00
Dahak Builds adbd2adf89 view.pick: map screenshot pixels to exact geometry (#6304) (#6329)
* claim: #6304

* view.pick: map screenshot pixels to exact geometry (#6304)

A read-only command that answers, for screenshot pixels (window or model
frame) or world-space rays: body, face index, exact world point and
outward normal, depth, occurrence/component for placed parts, and the
nearest edge/vertex within edge_px. Faces are raycast through the same
per-body pick index the mouse uses; flat faces are solved exactly
against their plane, curved faces take the mesh hit with the normal
interpolated from the mesh's surface normals. Section-aware: faces on
the removed side do not stop a pick and the cut cap answers with
section_cap. Never touches the selection, camera, document or undo.

Exposed as control ui.pick and as view.pick through engine.execute and
engine.batch (and through them MCP execute and batch).

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-25 01:24:39 +00:00
Dahak Builds 96303873fe Sketch: right-click toggle between driving and driven dimensions (#320) (#6357)
* claim: #320

* Sketch: toggle a selected dimension between driving and driven (#320)

A selected sketch dimension now offers 'Toggle Driven Dimension' in the
viewport right-click menu, running the existing engine command
sketch.toggle_driven. The over-constrain -> driven placement half landed
in #366; this exposes the engine's toggle in the UI, the remaining gap
from the retest notes on #320.

- context_menu: sketch_entity_items collects selected SketchConstraint
  ids that are dimensions (kind.value() is Some) and, for a single
  selected dimension, prepends the toggle item aimed at that constraint.
  Non-dimension constraints and plain entities get no item.
- Label reuses the existing catalog entry cmd.sketch_toggle_driven, so
  every locale translates it with no catalog change.
- Test: the menu offers the item for a selected dimension, running it
  round-trips driving -> driven -> driving, and a horizontal constraint
  or a plain curve gets no item.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-25 01:24:36 +00:00
NOWN-DEV b925a1af3d Configurations: a long table builds only the rows on screen (#6366)
* Test: a long configuration table builds the cells of the rows on screen

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

* Configurations: the table builds the rows on screen and its cells once per change

A table of hundreds of configurations was slow to open and to use: every frame cloned the
whole config.table answer, rebuilt every cell's text, looked each parameter's unit up once
per cell and built an editor for every cell of every row, on screen or not.

The answer and its cells (with units, looked up once per column) are now kept per design
revision, and rows off screen keep their space without building their cells, as the
parameters table already does. A row with an edit in progress stays live, so leaving its
cell still commits the edit.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:33 +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
NOWN-DEV c0640cb95e inspect.parameter_sweep: check a design across a parameter's range (#2273) (#2276)
* inspect.parameter_sweep: check a design across a parameter's range

Rebuilds a copy of the design at each value of a parameter and reports what
no longer holds compared with the design as it is: features that fail or
warn, joint origins that are lost or turn over on their own part, sketch
planes that turn over, and sketch solves that fail or fold. The design is
not changed; each value's rebuild time is reported too.

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

* inspect.parameter_sweep: params doc matches what it takes and returns

`steps` takes at most 199 (200 values), as the check says; failed and
warned entries carry the feature's name; a lost origin says which one; a
value the parameter refuses gives {value, ms, ok: false, error}. The
refusal test covers steps 200.

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

* inspect.parameter_sweep: Japanese and Chinese labels

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 01:19:00 +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
Victor Santos Oliveira ecf57d131a Make the generated command reference easier to browse (#5237) (#5721)
* claim: #5237

* docs: make the generated command reference easier to browse (#5237)
2026-10-25 01:17:36 +00:00
N3MI-DG 32ad6e0832 Command search: the closed field draws with the ribbon, under system windows (#5804)
The ribbon's command-search field had its own area at `Order::Foreground`, so
it painted above every system window: a Preferences card moved under the top
right corner showed the search field floating on top of it, while the rest of
the ribbon went under the card as it should.

Closed, the field is ribbon chrome and now draws at `Order::Background` with
the rest of the ribbon; open (or animating open or shut), it keeps
`Order::Foreground` so the results sheet still floats over every window.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-25 01:17:30 +00:00
Matt Ruhland 13c4a62a71 fix(skills): correct statements that no longer match 0.21.0 (#5899)
On 0.21.0 the facts test failed on rectangle symmetry, which now works
on two sides. Other statements in the modeling and mechanisms skills
also no longer match: edge picks have a distance limit, concentric
circles extrude as a ring, a hole sketch below a part drills into it,
slot ids count on from earlier curves, shell refuses curved blends and
lofts, measure and inspect_design grow with the body count, and
joint.sweep needs occurrences with against.

Each correction has a test in test_facts.py.
2026-10-25 01:17:23 +00:00
bherbruck bba1953623 Fillet/chamfer: find each edge on its own body (#6378)
* Fillet/chamfer: find each edge on its own body

A blend picked over edges of several bodies was evaluated on one body,
the one nearest the first pick, and every edge was named on that body
when the feature was made. Picks on the other bodies were named after
that body's nearest edge and rounded it instead (or moved to it by
position once an upstream edit moved them). With two stacked boxes, a
height change put the upper box's picks on the lower box's rounded
corners: the upper box's fillet resolved there and its own edges lost
their rounds.

Now each edge is named on the body it was picked on, and evaluation
finds each edge's body by its stored name (a gone name stays on the
feature's other bodies), groups the edges by body and blends each body.
A name an older build recorded on the wrong body (the pick lies outside
the box its reference recorded) is not used; the pick finds the edge.
A pick an upstream edit left at an end of its named edge goes to the
edge's middle, so the kernel cannot take the neighbouring edge.

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

* Fillet/chamfer: skip the per-body lookup when there is one body; name each body's picks in one pass

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 00:57:53 +00:00
bherbruck 0537623a75 Release 0.22.0 2026-10-24 18:53:43 -04:00
bherbruck 2a0096fe4a Release 0.22.0 (#6344)
* Release 0.22.0: version bump, design format 35

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

* Release 0.22.0: tests no longer expect the folded render and material changes to be pending

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:53:40 -04:00
fugazister 4b119dd471 Performance overlay: frame-time graph and histogram, triangles, draw calls (#2548) (#2549)
* Performance overlay: frame-time graph and histogram, triangles, draw calls (#2548)

Net change of the perf-overlay branch, rebased onto main as one commit.

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

* Document frame_rate_details in ui.prefs

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-24 22:50:43 +00:00
raykholo 6224cbf526 Thread: the refusal for a fine-only size says what to type (#4971) (#4977)
* claim: #4971

* Thread: the refusal for a fine-only size says what to type (#4971)

M28 and other sizes without an ISO coarse pitch were refused as an
unknown thread, with examples of other sizes. Thread and threaded Hole
now say that the size has no coarse pitch and suggest two to give, such
as M28x2 or M28x1.5. The coarse table is unchanged.

* Thread: automatic sizing past M56 says what to type (#5055)

Automatic thread sizing only knows the ISO coarse sizes up to M56, so a
75 mm shaft was refused with "no metric thread fits a 75.00 mm
cylinder", although M75x2 and M75x3 work when typed. Past the end of
the coarse range, the refusal now says so and suggests two sizes with
their pitch, scaled to the diameter (a hole's size adds one pitch to its
bore). The coarse table is unchanged.
2026-10-24 22:49:52 +00:00
dudedagger42 e89a8f56c1 MidPoint: the point goes to the line's middle, whichever is picked first (#5011)
MidPoint on a point and a line keeps the first pick still and moves the second.
With the point picked first (an end of one line, then the line it should sit in
the middle of), the line turned about its far end to bring its middle to the
point: a line from (0,0) to (20,0) went to (0,0)-(10,10) for a point at (5,5).
A point and a line now put the line first, so the point moves and the line
stays; the line still comes to the point when the point can't move.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:49:49 +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
raykholo 88f079bd8c Timeline: the tooltip hint is translated (Part of #4370) (#4398)
* claim: #4370

* Timeline: the tooltip hint is translated (Part of #4370)
2026-10-24 22:12:28 +00:00
imanu86 dbdeeec2e9 Undo restores what a hide put away (#2219) (#4780)
* Undo: what is hidden lives in the snapshot, so hiding is a step (#2219)

`Session::execute` records a step only when the document `Arc` changes, and what is
hidden lives in `Session::visibility` — saved with the design, but outside the
document. `browser.visibility` was also declared `.noundo()`, and `Snapshot` had no
field for it, so even a recorded step could not put a hidden body back: undo went to
the command before the hide and left it hidden (#2219).

`Snapshot` and `Checkpoint` now carry the `Visibility`, `go_to` restores it, and
`Session::execute` treats a visibility change as a change of its own. That change is
display-only, so it refreshes the view with a revision bump (`refresh_display`)
instead of evaluating a model a hide does not touch.

The command also takes `folder: "all"` (every body, sketch, construction item, origin
item and joint, plus the browser folders and occurrence hides) so Show All can be one
step, and asking for the state an item already has no longer reorders the hidden
lists: a redundant hide is not a step and leaves no unsaved changes.

* Show/Hide: the menus run browser.visibility, so the history has it (#2219)

The hide and show actions wrote `session.visibility` directly instead of running the
command: the body and face menus (`ui.hide` / `ui.show`), the `V` key
(`context_menu::toggle_visibility`), the browser rows' eyes, the Origin rows and
`ui.originAll` / `ui.originPart`. The engine could record a step and there was
nothing to record — which is why the release still answered "nothing to undo" after
hiding from the Browser.

They all run `browser.visibility` now (the repo rule that the UI acts through
commands), and `ui.showAll` is that one `folder: "all"` call, so Show All is one undo
step rather than three. `docs/occurrence-workflows.md` says what visibility does in
the history.
2026-10-24 22:12:17 +00:00
Sain2d 1dbb13cad2 Korean: translate the 297 messages added since the last pass (#5147)
assets/locales/ko.json had 298 ids that en.json has and it lacks (the
help.solid.* texts, the whole Preferences page, the issue-report dialog,
history names, ribs, mouse gestures and navigation presets). Add Korean for
297 of them; "Of" is left untranslated because its context is unclear.

Wording follows the existing ko.json (필렛, 모따기, 돌출, 쉘, 바디, 피처,
비활성화, 환경 설정, 어시스턴트, 에이전트). The file stays in the canonical
sorted one-message-per-line form; no existing message changed.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 22:11:59 +00:00
raykholo 0b91ff8a34 Measure picks projected and intersection sketch curves (#4337) (#4427)
* claim: #4337

* Measure picks projected and intersection sketch curves (#4337)

Dialog inputs turned away every curve or point of a sketch that is not the
active one, which includes projected and intersection curves, so Measure
took the nearest item it did accept instead. Give Measure explicit
acceptance for these references, keep the source sketch on a click, and
accept them from a pre-selection and a box selection. Other inputs keep
their restrictions.
2026-10-24 22:05:58 +00:00
raykholo 1e81e65e7e Browser tree hover stays fast on a nested STEP assembly (#3892) (#3894)
* claim: #3892

* Browser tree hover stays fast on a nested STEP assembly (#3892)

Hovering a placement row lit its whole subtree by walking it again
(Session::occurrence_bodies) on every frame, and opening a STEP file
makes its top placement active, so every highlight rebuild walked the
whole assembly again for the active placement's bodies. On a nested
assembly each walk resolves every placement below it, which cost
0.5 s or more per walk.

- Session::occurrence_bodies remembers its answers per placement until
  the document, the evaluated model or the pending moves change
  (occurrence_bodies_shared hands out the shared list).
- The highlight looks hovered bodies and faces up by name instead of
  scanning the hovered list once per body.
2026-10-24 21:59:55 +00:00
Christian Svensson 9227685483 Smooth sketch dragging on large imported faces (#3222) (#3480)
* claim: #3222

* Sketch drag: project a face sketch's links once per live gesture (#3222)

Every live sketch.drag frame refreshed the sketch's links before solving
the drag. For a sketch on a face that projects all of the face's edges
again: 115-460 ms per frame on the top face of a 600 x 500 mm plate with
120 holes imported from STEP, against about 1 ms for the drag's own solve.
The model the links come from cannot change during the gesture.

The session now remembers the sketch, document and revision a live frame
left. The gesture's next frame reuses the links while nothing else has
run: any other command (undo included) or any other change to the design
makes the next frame project them again. A test counts the projections.

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

* Sketch drag test: drop an unread assignment (#3222)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:53:32 +00:00
N3MI-DG 582e6097be Find in Browser finds the body; Find in Timeline opens the feature's folders (#3883) (#3884)
* claim: #3883

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

* Find in Browser finds the body; Find in Timeline opens the feature's folders (#3883)

A right-click on a body in the viewport picks a face, and the face menu's
Find in Browser selected the face's creating feature in the Features folder
instead of the body. It now reveals and selects the body, as the edge and
body menus already did.

Find in Timeline now opens any collapsed history folders around the feature,
and opens the Browser's Features folder for the picked occurrence, scrolled
to the feature (the reveal formerly behind Find in Browser).

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

* Find in Browser opens collapsed rows above the body and finds later placements (#3883)

Open the document, placement, Bodies and group rows when the command runs, not while the
Bodies folder is drawn, so a collapsed ancestor no longer hides the body. Match the scroll
target by the placed body's world name, and fix the group row key ({scope}/g{id}).

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

* Find in Browser picks the body's own placement; Find in Timeline leaves the Browser alone from rows (#3883)

Every component's first placement keeps the definition's body name, so the root placement
matched before the body's own one and Find in Browser on a first copy opened nothing above it:
the placement must belong to the body's component too.

Find in Timeline from a sketch, construction plane or feature row has no body to say which copy
was meant, so it opened the Features folder of whichever placement came first (the other copy),
or one that never lists sketches. From those rows it now only reveals the feature in the
History; from a face, edge or body it still opens that body's copy's Features folder.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:53:27 +00:00
raykholo 22190e703c Revolve, sweep, loft and hole from a nested STEP part's face join and drill it (#3223) (#3987)
With an opened STEP assembly's imported root active (it owns no body), a
Join revolve, sweep or loft from a sketch on a nested part's face made a
separate body, and a hole at the points of such a sketch found no body to
cut. A Join with no targets now takes the body its profile sketch is on,
through the same active scope as an extrude, and a hole counts as a cut
for the rule that a cut from a component with no body works on every body.

Refs #3223
2026-10-24 21:53:23 +00:00
leValdemar19 f7b08ab8c4 Dimension values: #name = value defines a parameter too (#2371) (#4212)
* claim: #2371

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

* Dimension values: `#name = value` defines a parameter too (#2371)

`#width = 40` in a dimension's value now works like `width = 40` (#2916):
it adds the user parameter `width` and dimensions with it. On `main` the
`#` made it an ordinary expression that failed to parse.

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>
2026-10-24 21:53:16 +00:00
TheMiddleCompany ae0b1edeac Coil: a section from a sketch profile (#4236) (#4239)
* claim: #4236

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

* Coil: store an optional sketch and profile selection

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

* Coil: sweep a sketch profile along the helix

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

* Coil: accept a sketch and profile selection

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

* Coil dialog: pick a sketch profile for the section

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

* Coil: cargo fmt

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

* Coil: refuse a loaded sketch coil that also sets the primitive section

A saved design could add section, section_position or start_angle to a
coil that already has a sketch; eval ignored them instead of refusing the
contradiction as the command does.

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

* Coil: assert the kernel refusal and exercise hostile profile selections

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

* Coil dialog: keep the sketch-profile check after rebase

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

* Format

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-24 21:53:13 +00:00
Nathalie Schönwetter cf1b0d67cc Extrude by pressing on a face or profile and pulling (#4767)
* feat: extrude a face or profile by pressing on it and pulling

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

* feat: pull-to-extrude keeps the window open and sets the step by sideways motion

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

* Pull to extrude: coarser step changes, a step ladder below the pointer, the step in the distance box

The step label sat on top of the distance box at the arrow tip. It is now a ladder of the
step in use between its neighbours, below the pointer, and the distance box also names the
step ("Step 1 mm"). One step change takes 44 px of sideways travel instead of 22.

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

* Pull to extrude: a setting, off by default (Preferences > Navigation)

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

* Rename the pull setting to Direct interaction

It will also cover other direct gestures in the viewport later (grabbing an edge to round it).

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

---------

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:52:58 +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 60105172b2 Sketch on a turned occurrence: reuse moved bodies across edits (#4853) (#4863)
* claim: #4853

* Sketch on a turned occurrence: reuse moved bodies across edits (#4853)

A sketch with a face anchor is evaluated in its component's frame.
state_in_frame_at moved every body placed differently from that frame
with a fresh transform on every evaluation, and the face-anchor frame
moved the anchored body again, so each sketch edit on a turned or moved
part transformed and re-meshed it.

The model now keeps the bodies it moved into a feature's frame, keyed on
the source body and the exact matrix (and checked against the source's
mesh, geometry, colour, paint and split lines, as world placement does).
An unchanged body under an unchanged placement is moved and meshed once.
Each evaluation starts a new generation and drops entries that neither
it nor the one before used, so the cache holds only placements in use.
Model states hold a weak reference, so the cache never keeps earlier
states alive.

Fixes #4853
2026-10-24 21:52:52 +00:00
Nathalie Schönwetter e2b8bf5a05 fix: copy a pattern's features in timeline order, not in the order they were picked (#4873)
A circular or rectangular pattern of a hole and the chamfers on its edges failed with
"no edge near ..." when a chamfer was picked before the hole: the chamfer was copied
first, onto a body that had no hole there yet.

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:49 +00:00
dudedagger42 9e065aa0d5 Viewport: keep the sketches' box per world state, not every frame (#5223)
projection() reads the model's box for the scene's depth, several times a
frame, and model_bounds() sampled every sketch's curves for it each time,
hidden sketches too. A 533-feature design with 445 sketches spent about 45 ms
of every frame there (stack samples: model_bounds -> Sketch::bounds -> segs ->
spline polylines), so orbiting ran near 16 fps. The sketches' box is now kept
for the world state it was made from, as the bodies' boxes already are.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:39:33 +00:00
raykholo 2fd7b6f08d Measure: a face or edge picked by its point alone (#4099) (#4114)
* claim: #4099

* Measure: a face or edge picked by its point alone (#4099)

inspect.measure (and the MCP measure tool) refused a face or edge item
without an index ("missing field `index`"). Face and edge items are now
read as body + point with an optional index: the point finds the face
(the same lookup face picking uses) or the nearest edge within the pick
tolerance, before the selection is built. A missing body or a point on
no face or edge is a clear error. Explicit indices still work.
2026-10-24 21:27:36 +00:00
raykholo 59795872f6 Extrude: document which way a positive taper goes (#4098) (#4115)
* claim: #4098

* Extrude: document which way a positive taper goes (#4098)

A positive taper slopes the walls outward (the far end is larger along the
extrusion direction), a negative one inward. Say so in the params doc, the
JSON schema and the guide, and pin the sign with a test.
2026-10-24 21:27:34 +00:00
Alexander Puzynia 37b7b0bd3e Offset free-form curves and mixed chains: trim folds, round corners (#2149, #2676, #2761, #3159, #3828) (#4156)
* Offset of free-form curves: trim folds, round chain corners

Offsetting a spline by more than its tightest bend failed with "offset spline
has no interior certificate correspondence": the plain offset folds over itself
there and a single linked spline cannot follow it. A chain of Bézier curves (any
imported SVG outline) was refused outright.

Such an offset is now the parallel boundary: the plain offset of every piece,
joined round each corner by an arc about it, trimmed where it folds or crosses
itself (a sharp corner is left), each kept run fitted with an open spline and
certified to the offset tolerance, and the whole proven free of crossings. Tiny
kinks are joined directly; zero-length Bézier handles (infinite curvature at
the end) are replaced by their chord within a quarter of the tolerance. A
single curve whose plain offset is smooth still gets the linked offset; the
trimmed or chained result is ordinary curves, not linked to the source, so the
command adds no distance parameter for it.

* Offset chains mixing lines or arcs with free-form curves

A chain offset read every curve with Sketch::exact_segs, which only covers free-form
curves, so a chain with straight or circular pieces (a text glyph, an SVG path with
L commands) failed with "offset source changed shape". Read them with exact_pieces,
which gives lines and arcs from their analytic form; the trimming already handles
those pieces.
2026-10-24 21:27:31 +00:00
dudedagger42 7e24f0e35a Sketch: a group too large to solve says so, not "conflicts with" nothing (#5280) (#5347)
The solver takes at most 4000 unknowns in one connected group, and a
pattern joins all its copies into one, so a hexagon patterned 17 × 20
(4000+ unknowns) failed as "that would over-constrain the sketch
(conflicts with )", naming no constraint. The solve report now carries
the size of the largest group it could not take, and the failure says
how many unknowns are joined, the limit, and that fewer copies or
several patterns or sketches stay within it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:26:40 +00:00
bherbruck 152efb5774 Windows installer: add the install folder to the system PATH (#6340)
MCP clients configured with "command": "solidcraft-cli" could not find it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:26:33 +00:00
rlartot e726f780d1 Sketch: the first dimension scales only a connected drawing (#3786) (#3815)
* claim: #3786

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

* Sketch: the first dimension scales only a connected drawing (#3786)

The first length dimension scaled the whole sketch about its origin, so a
second rectangle that nothing links to the first was resized and moved too.
Now the sketch scales only when its curves form one chain, joined by shared
points or constraints (the origin counts as a point); separate shapes keep
the previous behaviour: the dimension sizes the shape it measures.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:21:08 +00:00
Dennis Klappe b201c283c7 Model-cache tests trust an unknown source commit, so they pass without git (#4221)
Fixes #4045
2026-10-24 21:20:56 +00:00
dudedagger42 a2ae3ffbe0 Sketch drag: keep the far end of a tangent line still (#4925)
Dragging one end of a line tangent to a circle or arc let the solver move the
line's other end too, sometimes far ("goes flying"): only the dragged point was
held, and the least-squares step spread the change over the free points. The
drag now first tries holding the line's far end as well, and falls back to the
old solve only when that can't be met. Dragging the end of a tangent line to
(5, 12) moved its other end 0.399 mm on 0.19.0; it now stays put.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:20:15 +00:00
Dennis Klappe 96661111f9 OBJ export shares vertices and keeps body names as written (#4462)
Fixes #4451
2026-10-24 21:13:49 +00:00
dudedagger42 cc9d76cd86 Tapered cut: run on less, then start on the face, when the run on closes a small corner (#4825) (#4858)
A cut starting on a face of its target runs its tool on behind the start plane with the
tool's slope. A taper that widens the pocket narrows the tool there, and a corner arc
smaller than the run on times tan(taper) closed: "the taper closes the profile", though
the pocket itself only grows. The run on is halved, quartered, then dropped (the tool
starts on the face with its straight foot) until the tool builds.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:13:34 +00:00
dudedagger42 c61ffb54a8 Insert Part: M2, M2.5, M14, M24 and M30 screws, bolts, nuts and washers (#2091) (#5194)
The fastener tables stopped at M3 and M20. The socket head cap screw (ISO
4762), hex bolt (ISO 4014), hex nut (ISO 4032) and plain washer (ISO 7089)
tables, and the coarse pitches they use, now have M2, M2.5, M14, M24 and M30
too, in size order. Set screws and dowel pins are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:13:09 +00:00
raykholo ab889a8fff History: the sketch at the marker is shown while it's the current step (#5202) (#5204)
* claim: #5202

* History: show the sketch at the marker while it's the current step (#5202)

A sketch a later feature uses is hidden, so moving the History marker
onto it changed nothing in the view. The sketch immediately before the
marker is now drawn even when consumed or hidden; its stored visibility
is untouched, and earlier sketches, hidden components, groups and the
Sketches folder still apply.
2026-10-24 21:12:11 +00:00
dudedagger42 fcc3753a4c Canvas: a plane seen edge-on doesn't take the image (#4717) (#4876)
* claim: #4717

* Canvas: a plane seen edge-on doesn't take the image (#4717)

Placed with a click at a sketch's origin, the canvas went onto the origin's XZ square, which
is under the cursor there too and was the first plane hit. From the Top view that plane is
edge-on, so the image was out of sight and Canvas seemed to do nothing. A plane hit counts only
when the view shows it (its normal more than ~11.5 degrees off the view's edge-on direction); the
sketch plane, or another plane, takes the image instead.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:06:41 +00:00
Kai Evans 2c9d0a2847 Sketch palette: Finish Sketch sits inside the footer's inset, label centred (#5387) 2026-10-24 20:52:40 +00:00
Herperlerpicus 99fd05afeb Sketch Dimension: click an existing dimension to edit it while the tool is on (#5451)
With Sketch Dimension on and nothing picked yet, a click on a dimension
that is already in the sketch opens its value box, the same in-place
editor a double-click opens without a tool. Enter applies, Esc cancels,
and the tool stays on for the next dimension.

The dimension's text always takes the click; its lines only away from
sketch points and curves, which still start a new dimension (the rule
plain selection uses). Once a new dimension is under way, clicks keep
their meaning. Dimensions off in the selection filter are left alone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:52:35 +00:00
CamWight a8051760f7 Create Sketch in the right-click menu (#3032) (#3033)
* claim: #3032

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

* Create Sketch leads the right-click menu on empty space too (#3032)

A right-click on empty space in the viewport now starts with Create Sketch, in
the same place as a right-click on a plane (#1354): first, then a separator.
Over a plane it sketches on that plane, as before; on empty space it is the
toolbar's Create Sketch, so the next click on a plane or planar face starts the
sketch there. The menu offers it once.

Tests: empty_space_leads_with_create_sketch (the list) and
create_sketch_from_empty_space_sketches_on_the_plane_clicked_next (the harness:
right-click empty space, pick it, click XY).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:47:17 +00:00
github-actions[bot] c795a3f581 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-24 21:40:45 +00:00
Dennis Klappe 2cab9dc142 Measure: copy button next to the Distance value (#4442) 2026-10-24 20:46:51 +00:00
Dennis Klappe 6d8d53aac8 Browser tree: double-click a row to rename it (#4460) 2026-10-24 20:46:49 +00:00
n0tfr3shm1lk 4e6939b5cf Extend: extend a free arc in place and add the line the preview draws (#4615) (#4618)
* Slightly fix the extend tool

* Extend: add the line the preview draws, and stop an arc on another curve

sketch.extend reported "nothing to extend to" whenever nothing stood in the
line's way, even though the viewport previewed a line past its end; with
nothing in the way the extension now runs as far as the preview shows, on the
same measurement, so the click adds what was drawn.

An arc extension that ran into another circle or arc refused the target when
its crossing already belonged to another curve's end (a shared crossing point
of two circles): that point is now joined instead, so the extension closes onto
the geometry that is there.

---------

Co-authored-by: SolidCraft Dev <dev@solidcraft.local>
Co-authored-by: n0tfr3shm1lk <n0tfr3shm1lk@ukr.net>
2026-10-24 20:46:44 +00:00
GonMD f6ff2ff139 Sketch palette: the Tie button shows "Tie", not its catalog key (#4854) (#4857)
* claim: #4854

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

* Sketch palette: the Tie button shows "Tie", not its catalog key (#4854)

The button passed the key itself to ui_kit::button with Button::Normal,
which draws its label as given; it now passes the translated label. The
loose-ends test checks the button by its label.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:46:33 +00:00
dudedagger42 048c1ae8a8 Move preview: draw the moved bodies' own triangles, without evaluating the move (#4912)
A move changes no shape, but its preview copied the session, evaluated
the move and meshed the bodies again for every change of the values. While
the triad was dragged the part trailed the arrow by one of those
evaluations, and longer for a larger part. The preview now takes the
listed bodies' world meshes once and draws them moved by the move's
transform (about the world origin, as the Move feature turns bodies), the
original shown as removed and hidden as replaced; a copy keeps the
original. Anything it can't take (values that don't evaluate, a body not
shown, over 2 M triangles) goes to the scratch-design preview as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:46:29 +00:00
raykholo b00a1e87f4 Insert SVG: refuse geometry scaled to infinity, and bound the profile grid (#5020) (#5025)
* claim: #5020

* Insert SVG: refuse geometry scaled to infinity, and bound the profile grid (#5020)

A transform such as scale(1e308) turned a circle's radius into infinity.
The SVG reader now refuses non-finite transforms and geometry with a clear
error, and Grid::cells in the profile finder uses checked arithmetic, so an
infinite or huge box is checked against all shapes instead of wrapping the
cell count and allocating until memory runs out.

* Insert SVG: refuse lines with infinite ends; fix the grid test's cell count

A line whose transformed ends are infinite was dropped silently, because the
distance between them is NaN. It is now refused as out of range, like curves.

The new grid test expected one cell for a box spanning its own one-box grid;
such a box touches the 2 x 2 cells at its corners, so it expects 4.
2026-10-24 20:46:21 +00:00
Sandmann 8cbde001ef Front … Isometric View commands, Ctrl+1…7 by default (#5057) (#5058)
* Ctrl+1…7 turn the view to the standard views (#5057)

Front, back, left, right, top, bottom and isometric, handled with the
other view keys beside Ctrl+8: animated like the view cube, reserved so
no command can take them, and ignored on the flat pattern page.

* Front … Isometric View are commands, Ctrl+1…7 their default keys (#5057)

The standard views become seven commands (view.front, view.back,
view.left, view.right, view.top, view.bottom, view.isometric) whose
default shortcuts are Ctrl+1…7, instead of keys the window handles
itself. They show in Keyboard Shortcuts, can be moved to other keys or
cleared, and the digits can be given to other commands. In the app they
turn the view as the view cube does; headless they set the session
camera, keeping target, distance and projection.
2026-10-24 20:46:18 +00:00
dudedagger42 ad8b2f91bb File: a saved model whose booleans retried is taken on opening (#5286) (#5334)
A design file keeps its evaluated model so that it opens without
evaluating the timeline. A feature whose boolean needed a retry records
it, and the kernel's BooleanDiagnostics leaves `rejected` out when it is
empty but has no default for it when read, so reading those records
failed ("saved model: diagnostics"), the saved model was refused, and
the design was evaluated on every open (17.6 s for the reporter's
17-feature design, 1.2 ms with this).

The reading side now takes an absent list as an empty one, without a
change to the kernel crate.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:46:06 +00:00
Matt Van Horn 9919825ea3 Expand All and Collapse All in the Browser (#5190) (#5335)
Open or close every Browser folder from the design or a component menu,
including folders that start closed. Display only: not saved, not an undo step.
2026-10-24 20:46:04 +00:00
Kai Evans 97e0ff9050 Sketch: a face seen edge-on projects as one line end to end (#5375)
A face orthogonal to the sketch plane projected every edge onto the same
line: coincident and overlapping pieces, one per notch, that the face
boundary check then refused on the next evaluation ("boundaries cross,
touch or have ambiguous topology") and marked lost.

A link layout whose straight pieces all fall on one line is now one line
between its two farthest points; vertices along it can be projected one
by one. The face-boundary and loop-identity checks skip such a silhouette,
which encloses nothing. Coincident lines are laid out once.
2026-10-24 20:46:01 +00:00
Adam Mikołajczyk c48c1a09d3 fix(skills): quote editing description for valid YAML (#4389) 2026-10-24 20:43:05 +00:00
dudedagger42 ee3d41210d Orbit about the cursor: over empty space, turn about the part in view (#2616) (#4947)
* Orbit about the cursor: over empty space, turn about the part in view (#2616)

With Orbit around → Point under the cursor, an orbit begun over empty space
turned about the middle of everything shown. For a part far from the origin and
anything at the origin (a small body, a sketch), that middle is halfway between,
far off screen, and the part flew out of view: a box at (50000, 50000, 0) with a
10 mm box at the origin went from (872, 535) on screen to (-277084, -279746) after
a 180 px orbit. It now turns about the middle of the bodies the view shows part
of, and only falls back to everything shown when no body is in view.

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

* Restore LF line endings in the files this change edits

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:39:54 +00:00
raykholo be685d064f Add a contribution skill for first-time contributors (#5851) (#5852)
* claim: #5851

* Add a contribution skill for first-time contributors (#5851)

skills/solidcraft-contributing walks a new contributor from checking their
tools through fork setup, choosing and claiming an issue, a focused change
with checks and screenshots, and review. The skills index lists it and
the install commands point at tangency-labs/solidcraft.

* Skills install: copy the modelling skills only

The contributing skill is read from its link when someone wants to
contribute, so the install commands no longer put it in every project's
global skills folder.
2026-10-24 20:39:27 +00:00
Karl Von Richter 32e613aa8c Home: test that a sample's picture arriving while it builds is not a failed build (#4351)
#4349 fixed the start page treating a sample's shipped picture, which comes back under the
sample's key without a design, as its build failing. That race dropped the request with "the
sample could not be built" and made the s15_start_page_sample_save_reopen scenario fail
depending on timing. This adds the test that sends the picture while the sample is wanted and
checks that the sample still opens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:25:58 +00:00
Kai Evans 58a9d33f02 Right-click menus stay open when the opening click's press and release share a frame (#2458) (#2460)
A menu ignores presses until it has been on screen for a frame, so the
press of the click that opened it can't close it again.

Fixes #2458.
2026-10-24 19:25:56 +00:00
raykholo a8e6154c0d Pattern of features: show the direction arrow on the patterned feature (#3034) (#3909)
* claim: #3034

* Pattern of features: show the direction arrow on the patterned feature (#3034)

A rectangular pattern whose Object Type is Features had no drag arrow
and no value box: the canvas anchor knew bodies, faces, edges and
sketch geometry but not a whole feature, so it returned nothing.

A picked feature now anchors at the middle of the geometry it made
(its bodies, or its faces on a body later features changed). The
pattern's preview stands in for the body the feature lies on, so the
anchor reads the feature's geometry including replaced bodies, as a
body pick reads its body.
2026-10-24 19:25:52 +00:00
Mateusz Sych d61dff69a6 Exploded views export as an animation (#3713) (#3744)
* claim: #3713

* Exploded views export as an animation (#3713)

motion.export_animation takes an exploded view (view: id|name) as well as a
study or a joint: the parts come apart at a steady pace from the assembled
placement to where the view shows them, and back together with back: true
(exploded at the middle frame). 41 frames by default, 81 with back. The home
view frames the parts both assembled and exploded. As with explode.show, the
design keeps its placements, its history and its saved state.

explode.show and the export share the moves; moves a design file gives with
no finite translation are left out.

* Mechanisms skill: an exploded view's default frame count

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-24 19:25:49 +00:00
jason3w 8c853fff3e Press Pull: route curved faces to Offset Face (#2873) (#3463)
* claim: #2873

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

* Press Pull: route curved faces to Offset Face, with an arrow (#2873)

Q sent every non-edge selection to Extrude, which takes only profiles and
planar faces, so a hole's wall or a boss's side was refused. The engine's
solid.press_pull already offsets curved faces; the window now does too:

- Q on a curved face opens Offset Face; its Faces input takes curved faces.
- Q with nothing selected opens Extrude as before, and a first pick of a
  curved face or an edge reopens Press Pull on it (Offset Face or Fillet).
- Offset Face's arrow follows a curved face's outward normal where it was
  picked, so it can be dragged like an extrude's.

Fixes #2873

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:12:40 +00:00
Homero Thompson daec1091f4 Keyboard Shortcuts: a bound Shift key is named "Shift", not "Shift+ShiftLeft" (#4582) (#4657)
* claim: #4582

* Keyboard Shortcuts: a bound Shift key is named "Shift", not "Shift+ShiftLeft" (#4582)

The shortcut's name was the modifiers held plus the key itself, so binding the
Shift key alone showed "Shift+ShiftLeft": the modifier named twice, and the
left and right keys as two different shortcuts.

A modifier key is now the shortcut itself, and its two sides are the same one
(the app's modifiers collapse them), so the page shows what the user pressed.
A shortcut bound before the change ("Shift+ShiftLeft") keeps working: the
stored name is migrated when the preferences or a shortcuts file are read.

* Keymap: the bound-modifier test passes the new repeat flag to press

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 18:59:04 +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
froglemonade2 46f24f2394 Save the browser's view with the design, and an option to hide the origin by default (#1849)
The Origin folder's eye, a finished sketch's Show Profile and Show Dimensions,
and body locks move from UiState into the design's saved visibility
(origin_hidden, hidden_profiles, shown_dimensions, locked_bodies), so each tab
keeps its own and they survive closing the design. The Sketches folder uses the
saved browser folder (c0/sketches in hidden_folders); ui.set/ui.inspect keep
showOrigin, showSketches, hiddenProfiles, shownDims and lockedBodies.

Preferences > Design > "Hide origin by default": a design that never toggled
its Origin eye follows the preference. New fields are a pending format change
(format/pending/browser-view-state.rs).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:15:47 +00:00
raykholo 1236863b17 Coil: its parameters show and can be edited (#3862) (#3915)
* claim: #3862

* Coil: its parameters show and can be edited (#3862)

The Coil dialog now shows every setting solid.coil takes: the section
(circular or square), where it sits on the diameter (inside, center or
outside), the start angle and a Clockwise option. Edit Feature on a coil
reopens the dialog filled in from the feature, and OK redefines it;
before, it fell back to a timeline.edit line in the command search.
Settings left at their default are not sent, so OK on an unchanged edit
keeps the coil as it was.
2026-10-24 17:02:30 +00:00
cronos 4f4b1fc2d2 Sketch: select edge midpoints for horizontal and vertical constraints (#5159) 2026-10-24 16:15:45 +00:00
Rec0iL d5a79a77e6 Agent chat: Pick context and Attach selection for elements, features and bodies (#2286) (#2288)
* claim: #2224

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

* Agent chat: keep Send in view for a long message, open "View attached" (#2224)

The message box sat in a fixed 92-point reserve, so a long message grew it
past the panel and took Send, Stop and Attach view with it. The box is now a
panel of its own at the bottom: it grows upwards up to a cap, then scrolls,
and the conversation takes what is left.

The picture sent with a message is kept on it (once the agent starts on the
message), and the "View attached" caption opens it in a window.

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

* Agent chat: a message's pictures as thumbnails with a hover preview and a viewer (#2224)

Instead of a window on the far side of the screen: the pictures of a message
show as small thumbnails under it, hovering one shows it bigger for as long as
the pointer stays, and a click opens a viewer over the app with the message's
pictures one at a time (previous and next, arrow keys, Esc). A picture carries
a note shown with it, so attachments of other kinds can use the same viewer.

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

* claim: #2286

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

* Agent chat: Attach selection sends what is selected in the model with the message (#2286)

A checkbox beside Attach view. The selection is read when the message is
sent (also for a queued one) and described: faces and edges with the names,
indices and points the tools take plus their size and place, features by name
and kind, bodies by name; at most 64, the rest counted. The agent gets it as
text after the message; the conversation shows the message as typed with the
things as grouped chips (hover for what the agent was told).

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

* WIP: the chat's context picker dialog (#2286)

* Agent chat: Pick context, a picker for elements, features and bodies (#2286)

A button beside Attach selection opens the picker: a dialog with Elements,
Features and Bodies inputs, so clicks in the model, the timeline and the
browser pick into the active one (clicking again takes one out), with the
usual highlight. OK puts the picks as chips with an x above the message box;
they go with the next message like an attached selection (text for the agent,
chips in the conversation) and the tray is empty again afterwards. Cancel
keeps what was picked before. The control channel opens it with pick.

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

* Agent chat: Attach selection counts and describes what was clicked, not the pieces of a rim (#2286)

A click on a circular rim or a split surface selects all its pieces for the
commands. The checkbox counted them one by one and the agent got one entry
per piece (reported on macOS). They are now grouped back into the click they
came from: one chip, one entry with the whole size, and the indices of every
piece in its detail. Edge details are looked up by position as well as
index.

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

* Agent chat: a message's picture is kept on exactly that message (#2224)

The turn now carries its message's place in the conversation (also through a
queue and a send again after a sign-in), so its picture is kept there, once,
and never on an older message that also wanted one (review of #2225).

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

* Agent chat: use the UI kit's checkbox and button and the font tokens (guard tests)

Main's UI-kit guard tests reject raw egui checkboxes, buttons and font sizes in the
chat panel; Attach selection, Pick context, the chip remove button and the chip fonts
now come from ui_kit and Tokens.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-24 15:57:24 +00:00
bherbruck 5d47946e4d Downloads: say Windows code signing comes from SignPath Foundation (#6207)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:30:33 -04:00
github-actions[bot] 69cfd89ae8 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-24 15:17:54 +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
phanoob 5ca7c2fb95 Sketch tools: pressing a tool's shortcut again cycles its creation modes (#2459) (#3564)
* claim: #2459

* Sketch tools: pressing a tool's shortcut again cycles its creation modes (#2459)

R starts 2-Point Rectangle; R again, before the first corner, switches to
3-Point, Center, Rounded, then back to 2-Point (likewise for Circle,
Line, Arc, Slot, Polygon and Spline keys). A held key's auto-repeat
doesn't cycle, the first press always starts the bound mode, and once a
point is placed the key keeps the tool and its points.

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

* Sketch tools: the mode cycling is a Sketch preference, off by default (#2459)

A tool's shortcut pressed again keeps restarting the tool, as before;
Preferences > Sketch > "Pressing a tool's shortcut again switches its
mode" turns the cycling on.

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>
2026-10-24 11:46:46 +00:00
cronos 592f6a6fee ci: fix local scheduling and Windows test baselines (#4941)
* ci: repair local nextest scheduling and Windows fixtures

* test: refresh MSVC robustness references and isolate GNU policy

Integrate the Windows reference records from #3361 and the target-environment
selection and recording policy from #4159. Keep shared records and comparison
tolerances intact, and retain seed 36 as an existing failure tracked by #3583.

Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: chris <christopher.nourse@gmail.com>

* test: make CI subprocess and revolve fixtures deterministic

---------

Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: chris <christopher.nourse@gmail.com>
2026-10-24 11:33:10 +00:00
DuBearte 4612c5e6a9 Equal with three or more curves selected makes them all equal (#4985) (#5139)
* claim: #4985

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

* Equal with three or more curves selected makes them all equal (#4985)

With more than two curves selected, Equal fell back to the pick tool and
applied nothing. sketch.constraint.equal also takes curves: [2..200],
each made equal to the first in one step (strict, so a conflict changes
nothing), and the toolbar sends a selection of three or more curves as
that list.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:26:38 +00:00
DuBearte c0da9fe861 Trim cuts a line at its tangent point on a circle (#5074) (#5163)
* claim: #5074

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

* Trim cuts a line at its tangent point on a circle (#5074)

A line the solver made tangent to a circle touches it within rounding
noise, and the exact intersection missed the touch whenever the noise
fell outside, so trim found no cut there and deleted the whole line.
Trim takes its cut points from intersections_within at its own merge
distance, as profile detection does (#588).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:26:36 +00:00
amolinari-autotex 19db763899 Tapped Hole: verified metric fine, UNC/UNF and parallel G/Rp catalogues (#4178)
* feat: expand tapped-hole catalogue with metric fine and Unified sizes

* docs: distinguish metric designations from expanded tapped catalogue

* feat: add internal pipe catalogue and checked Rc profile data

* Connect parallel G and Rp catalogues to cylindrical tapped holes

* fix(i18n): register tapped thread catalogue labels

* fix(i18n): complete thread labels in required locales
2026-10-24 10:22:57 +00:00
Mateusz Sych 9a737bd63c Linked components: joints keep their face after Update Links (#3330) (#4874)
* claim: #3330

* Linked components: joints keep their face after Update Links (#3330)

A linked design comes in as STEP text, which keeps no face names, so the
faces of its bodies were named by their place in the file (`face:<k>`).
An edit of the part that adds a face earlier in the file (a second hole
of the same size) gave that place to another face, and a joint on the
linked part moved there silently.

When a link is made or updated, each imported face is matched to the
part face it was written from (the same geometry), and the Import
feature keeps that face's name in the part (`Feature::link_faces`, a new
pending format change). The linked face is named
`F<import>🔗<digest of that name>`, pieces keeping their qualifiers,
so references follow the part's own history through Update Links as they
do inside the part, and a face the part no longer has is reported.
Linked faces still answer to `face:<k>`, so older references keep
resolving; Update Links warns about joints that use them and reports
joints that no longer solve.

* Linked faces: a solid's twin keeps its own names; renamed faces count as an update

Two solids of a part alike in one place each take the names of the solid
the file names them after. Update Links reports a link whose part named
its faces differently since as updated (a joint may follow that name to
another face), not as unchanged; only a link getting its first names
stays unchanged.

* Linked faces: Extract to File carries the design's face names too

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

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:33:36 +00:00
Daniel 8b65d41b31 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>
2026-10-24 08:01:48 +00:00
Martin Pålsson 4dc30c092d 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>
2026-10-24 07:55:14 +00:00
SS-EN e67ae66f3e 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>
2026-10-24 07:35:31 +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
Jacques 807e367bdb 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>
2026-10-24 05:32:30 +00:00
Jacques 400e06c202 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>
2026-10-24 05:25:56 +00:00
Frank 1fa0371c75 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>
2026-10-24 04:13:39 +00:00
Frank fc3e3b6c8d 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>
2026-10-24 04:13:31 +00:00
github-actions[bot] d1a6262861 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-24 04:15:11 +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
Frank d2eb8cf639 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>
2026-10-24 03:40:39 +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
github-actions[bot] 46fca160e1 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-24 02:34:23 +00:00
rbousigues88 16727f331c 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>
2026-10-24 01:40:56 +00:00
bherbruck 01220d3eb5 Version 0.21.0 on main (released from rc.2's code) (#5774)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:05:06 +00:00
Colin Faulkingham 89c1f02850 Sketch slots: provide typed dimension inputs for every variant (#2423) (#4318)
* claim: #2423

* fix(sketch): add typed dimensions to every slot tool (#2423)
2026-10-24 00:59:27 +00:00
cronos 45edd5baf5 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.
2026-10-24 00:58:56 +00:00
Colin Faulkingham 7dd5baac5c Browser: route body picks into open command dialogs (#2560) (#4371)
* claim: #2560

* fix(ui): route Browser body picks into active dialogs
2026-10-24 00:52:31 +00:00
bherbruck 65d288227e Release 0.21.0 (rc.2's code, version only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:51:08 -04:00
Colin Faulkingham 2e5924ed5e Preserve construction references when axes and points are renamed (#4419) (#4449)
* claim: #4419

* fix: preserve renamed construction references (#4419)
2026-10-24 00:24:51 +00:00
Kai Evans a199a2cc25 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.
2026-10-24 00:11:38 +00:00
Nathan Dane fb374b543b 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
2026-10-23 23:57:34 +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
Artem Sushchev 30b01e8d79 A spherical shell revolves into a solid with a void, and cuts and joins with hollow tools work (#451) (#4182) 2026-10-23 22:53:42 +00:00
Diogo Soares Rodrigues 1ed394f8d6 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>
2026-10-23 22:03:23 +00:00
Homero Thompson d5a046c3d3 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`.
2026-10-23 21:25:09 +00:00
raykholo 4353f34bd1 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.
2026-10-23 21:25:02 +00:00
bherbruck fedb3bcdb1 Release 0.21.0-rc.2 2026-10-23 17:14:38 -04:00
bherbruck a51dfc9e1f Release 0.21.0-rc.2 (#5691)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:14:35 -04:00
bherbruck 9aaa8016e5 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>
2026-10-23 17:04:32 -04:00
Colin Faulkingham 71f7a8e697 Move: scope box selection to the active component (#4459) (#4527)
* claim: #4459

* fix: scope Move box selection to active component (#4459)
2026-10-23 20:25:00 +00:00
bherbruck 632ec4157c 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>
2026-10-23 16:11:39 -04:00
ealitt 6eaf723041 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>
2026-10-23 20:05:58 +00:00
bherbruck c3ed8b1c07 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>
2026-10-23 16:05:46 -04:00
Jackson Oursland 8301d1c48e 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>
2026-10-23 16:05:41 -04:00
github-actions[bot] 2a02eb71ac Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 20:04:02 +00:00
bherbruck 097b8b7550 Release 0.21.0-rc.1
The tree is main at f120285c9 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:48:05 -04:00
bherbruck f120285c96 Fix design format 33 for 0.21.0-rc.1 (#5610)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:47:48 -04:00
bherbruck af7ca3a8a2 Release 0.21.0-rc.1
The tree is main at b31fddb98 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:37:43 -04: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
raykholo bb155d446a 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.
2026-10-23 17:22:24 +00:00
bherbruck 27ddb725c7 Release 0.21.0-rc.1
The tree is main at 2bf3d4e11 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 12:10:02 -04:00
bherbruck 2bf3d4e11f Release 0.21.0-rc.1 (#5566)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:09:47 -04:00
github-actions[bot] 9852a2e758 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 16:05:55 +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
ahmetalpcelik 4bdeaacde6 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>
2026-10-23 15:35:54 +00:00
raykholo e5dffc7052 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.
2026-10-23 15:18:58 +00:00
Thodoris Tsagkogeorgas 43fa25458d 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.
2026-10-23 14:55:39 +00:00
phanoob 9b8fa3d357 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>
2026-10-23 14:48:11 +00:00
raykholo 755ad9af07 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.
2026-10-23 14:45:45 +00:00
Christian Svensson 46aa3d5d47 STEP import: let a cancelled import release the import slot (#3171) (#3189)
* claim: #3171

* Release cancelled file import slots (#3171)
2026-10-23 14:31:51 +00:00
rlartot 125b8d5387 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>
2026-10-23 14:31:48 +00:00
phanoob b5edd98bc9 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>
2026-10-23 14:31:46 +00:00
Victor Santos Oliveira 647a52d0fb 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.
2026-10-23 14:31:42 +00:00
github-actions[bot] 8c977131d9 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-24 00:20:18 +00:00
AhmetV 30a1fe74ca 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>
2026-10-23 14:31:30 +00:00
Tyler Chiriboga fa2c1c31f6 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>
2026-10-23 14:31:27 +00:00
leValdemar19 7e472bc4da 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>
2026-10-23 14:31:22 +00:00
Loohi 145c6ad60d 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>
2026-10-23 14:31:20 +00:00
raykholo e1bf784014 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.
2026-10-23 14:31:08 +00:00
Colin Faulkingham 1bbc5ab0f7 Keep long Select Other labels inside the popup (#4209) (#4225)
* claim: #4209

* fix(ui): bound Select Other labels and expose full names (#4209)
2026-10-23 14:30:57 +00:00
raykholo c1e9d2f057 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.
2026-10-23 14:30:54 +00:00
CaRLymx 68abb15d7f Spanish (es-ES): catch up with en.json (#4292)
Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
2026-10-23 14:30:51 +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
raykholo d6b1608f9d "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.
2026-10-23 14:30:32 +00:00
raykholo 2d54cb31bc 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.
2026-10-23 14:30:27 +00:00
Frank 3acb81964a 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>
2026-10-23 14:30:21 +00:00
raykholo 4e7bf2b63a 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.
2026-10-23 14:28:23 +00:00
Kai Evans 3093ce1d87 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>
2026-10-23 14:23:13 +00:00
phanoob ca9dd02201 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>
2026-10-23 14:23:04 +00:00
raykholo 1d9bdb1ca3 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
2026-10-23 14:23:02 +00:00
github-actions[bot] d78494e016 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 22:44:53 +00:00
phanoob d18cc0df4e 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>
2026-10-23 14:22:59 +00:00
Peppe Bergqvist 04dc68d988 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>
2026-10-23 14:22:57 +00:00
HiroKenEz efb45d7411 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>
2026-10-23 14:22:52 +00:00
raykholo 426f8ddd18 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.
2026-10-23 14:22:45 +00:00
Karl Von Richter 27d2fe6ec0 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>
2026-10-23 14:22:41 +00:00
raykholo 6fb72c7401 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.
2026-10-23 14:22:37 +00:00
raykholo 14ccc71291 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.
2026-10-23 14:22:35 +00:00
raykholo ec56df3ffd 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.
2026-10-23 14:22:33 +00:00
CamWight 8565f3f1b3 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>
2026-10-23 14:22:29 +00:00
Colin Faulkingham 12c823ffdd Browser: add Rename and Delete to feature menus (#3839, #4343) (#4342)
* claim: #3839

* fix(ui): add Rename and Delete to browser feature menus
2026-10-23 14:22:17 +00:00
SS-EN 5d92ad1e23 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>
2026-10-23 14:22:13 +00:00
CamWight e8a6a5d3ff 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>
2026-10-23 14:20:12 +00:00
raykholo 605d32ceb0 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.
2026-10-23 14:17:30 +00:00
Frank b79c03d227 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>
2026-10-23 14:14:48 +00:00
raykholo 17bac141c8 An intersect that would leave nothing is refused instead of deleting the body (#5295) (#5303)
* claim: #5295

* An empty intersect is refused instead of deleting the body (#5295)

An intersect whose boolean came out empty removed the target body and
reported success. The shared evaluator now refuses it when nothing of a
named target would remain, when nothing of any target would remain, or
when the tools meet a target and the result is still empty. An unnamed
target the tools miss altogether is still removed, as an intersect with
every body does (#5160). Sheet metal flat machining follows the same rule.

* Sheet metal: an intersect with already removed material is refused (#5295)
2026-10-23 13:50:47 +00:00
gsrunion 4b42ef50c7 Mesh leftovers from the ribbon redesign (#133) (#4471)
* claim: #133

* mesh: Save As Mesh declares the panel that shows it (#133)

The ribbon redesign (#2176) removed the MESH tab, but three things still
pointed at it:

- `file.save_mesh` declared `.at("MESH", "MODIFY")`. The command inventory
  prints the placement a spec declares, so `docs/features.md` advertised a
  `MESH > MODIFY` location no ribbon has. Its real home is already
  INSERT / FILES (`SECTIONS` lists it), so the declaration now agrees with
  the ribbon; `destination()` resolved it there before and after, so nothing
  moves on screen.
- The user guide sent people to "the Mesh tab under Modify" for Save as mesh.
- A test now refuses any command that declares a tab the ribbon removed, and
  pins where Save As Mesh lives.

`PLACEMENTS` keeps its MESH entries: those migrate toolbars users saved under
the old panels, which is a different question from what a spec declares.

---------

Co-authored-by: Geraldrunion <geraldrunion@users.noreply.github.com>
2026-10-23 13:45:35 +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
Colin Faulkingham 74ea81519b Keep Parameters responsive in large feature histories (#2733) (#4549)
* claim: #2733

* Keep Parameters responsive with many feature groups (#2733)
2026-10-23 13:45:26 +00:00
SS-EN 769dfff1a5 Section Analysis: a Hatch option (#4563) (#4565)
* claim: #4563

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

* Section Analysis: a Hatch option, off for a plain cut in the body's colour (#4563)

The Section Analysis dialog gets a Hatch check box beside Flip, and
inspect.section a hatch?: bool (default true), so scripts, the CLI and the
control channel reach it too. Hatched, a cut is its body's colour tinted
toward the cap colour with 45 degree stripes, as before; off, it is the
body's own solid colour. The viewport (cap alpha in the uniform), the CPU
rasterizer and ui.render follow it, and the section keeps it when hidden
and shown again or edited.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:23 +00:00
SS-EN 3ef68c708f Expression suggestions stay closed while another control is dragged (#4589) (#4590)
* claim: #4589

* Expression suggestions stay closed while another control is dragged (#4589)

A drag elsewhere leaves a text box its focus (egui gives it up on a click,
not a drag), so dragging the Drive Joints slider rewrote the focused Angle
box under its old selection. A letter before the caret ("5.0 d|eg") made a
new word, a fresh undismissed popup, and the list opened over the slider.

Dismissed suggestions now stay closed until the box is typed in, a drag of
another widget dismisses them, and a hidden list forgets its row rects.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:17 +00:00
Reefwalker 3ed1829cc7 fix: retain circular sketch pattern Quantity expressions (#4435) (#4607) 2026-10-23 13:45:15 +00:00
raykholo b982ccd8c4 Mesh Convert works without activating the mesh's component first (#4609) (#4613)
* claim: #4609

* Mesh Convert works without activating the component first (#4609)

Inserting a mesh puts it in a new component while the parent stays
active, so Convert Mesh always refused the first time. Convert now
resolves the mesh body's owning component and adds the conversion
there when the active component is that component or an ancestor,
leaving the active component unchanged. From another branch it still
refuses, and the error names the component to activate.
2026-10-23 13:45:13 +00:00
dudedagger42 2586b09f5a Control: a whole-design measure reads a copy off the window thread (#4604) (#4614)
* Control: a whole-design measure reads a copy off the window thread

`inspect.measure` and `document.inspect` (which measures every body by
default) ran on the window thread. On a large assembly that took about a
minute, the window answered nothing meanwhile, and Windows closed it as
not responding (#4604). They change nothing, so they now run on a copy of
the session on a worker and are answered from there, like exports, but
whatever the "agent commands off the window thread" preference says: an
answer from the design as it was when asked has no order to keep. The
window keeps answering `ui.*` and drawing meanwhile.

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

* Control: clearance and interference checks read a copy off the window thread too

inspect.clearance and inspect.interference only read the design, and on a
large assembly measure every pair of bodies: on a 153-body design a clearance
check computed for over five minutes on the window thread, the window showing
"not responding". They join inspect.measure and document.inspect in running on
a copy, answered from there.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:10 +00:00
DuBearte a5f9ff45dd Sketch Symmetry: a dimensioned shape slides onto the axis (#4030) (#4626)
* claim: #4030

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

* Sketch Symmetry: a dimensioned shape slides onto the axis (#4030)

With a rectangle's width dimensioned, Symmetry of its two sides about an
axis was refused: the second side can't move on its own, the axis never
moves, and the ordinary solve that slides the whole rectangle only ran
when every first-picked entity could move. One movable first entity is
now enough.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:08 +00:00
raykholo 61360c3b0d Convert to Sheet Metal: a bend picked on its tangent edge connects, on rotated parts too (#4328) (#4630)
* claim: #4328

* Fix sheet conversion picks on tangent bend edges (#4328)

Resolve cylindrical bends through the nearest edge's incident faces when a
pick on a tangent boundary resolves to its planar panel. This keeps valid
remote bends connected, including picks on the other skin of a shell.

Keep conversion geometry and picks in the body's local frame, and format
conversion diagnostics through its world placement. Cover rotated bend
picks and disconnected-pick coordinates with synthetic regressions.
2026-10-23 13:45:06 +00:00
Reefwalker eec6da060f Measure: report obtuse angles at a shared corner (#4625) (#4634)
* claim: #4625

* fix: measure obtuse corners independently of edge direction (#4625)
2026-10-23 13:45:04 +00:00
DuBearte f422bb1263 Intersection edges divide at 1e-6 when asked for less, instead of panicking (#4623) (#4650)
A join failed with the kernel's internal error "tolerance must be no less
than 1e-6" at vendor/truck-geometry/src/decorators/intersection_curve.rs:172:
IntersectionCurve::parameter_division asserted truck's minimum tolerance,
and the guard turned the panic into a failed operation. A tolerance below
truck's TOLERANCE, or one that is not a number, now divides at TOLERANCE:
the curve's points are only found to within it anyway.

The test divides the intersection edges of a hole cut through a B-spline
face at 5e-7, 1e-12, 0, -1 and NaN: each gives the division at 1e-6. Without
the fix it panics with the reported message at the reported line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:56 +00:00
Martin Pålsson 96ce31153c Pattern and mirror: a rib as the source (#4593) (#4667)
* claim: #4593

* Pattern and mirror: a rib as the source (#4593)

A pattern or mirror of features refused a rib as a source ("Rib1
cannot be patterned or mirrored yet"). A rib's walls run until they
meet the body, so each copy is built again where it is: the rib's
sketch is moved by the copy's transform and the walls are run against
the bodies there, as a hole is drilled again at each copy. The copies
join the body, as the rib does. The rib's own tool building moves into
rib_tools, which the rib feature calls unmoved.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:52 +00:00
Steve Jason a9c3c8bb8a Accept a leading = in expressions, so a fillet radius can be =d3 (#4681) (#4686)
* Sketch fillet radius: accept a leading = in an expression (#4681)

* A second = after a name is still not a definition
2026-10-23 13:44:48 +00:00
dudedagger42 a147a7c083 Sketch Point: a point put on a midpoint or a curve snap is held there (#4691) (#4745)
* Sketch Point: a point put on a midpoint or a curve snap is held there (#4691)

The Point tool sent only a snapped point's coordinates and sketch.point
took only [x, y], so a point clicked on a line's midpoint (or on a curve,
or a model vertex) was free to move off it. sketch.point now takes the
snap forms the other draw commands take ("mid:<line>", "on:<curve>:x,y",
{vertex}, {on_edge}); the snap makes the point with its constraint. The
tool passes those snaps as themselves; a snap to an existing point still
goes as coordinates, where `connect` joins them.

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

* Sketch Point: refuse a snap it can't read instead of answering as if it made a point

sketch_project::snap_points leaves a snap it can't parse ("on:c1:abc",
"on:c1", "on:c1:1,2,3") as it was given, and sketch.point then read
that string back as the point it had made: no error, no point, and an id
a script would go on to dimension. It now takes the snap only when it
names a point of the sketch, and otherwise refuses, listing the snap
forms (found by the test agent on #4745).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:42 +00:00
dudedagger42 89a11d75c0 Imports through the app keep their matrix, name and options (#4676) (#4746)
The app's background loader (file_load::control) kept only `up` and
`names` of a control-channel open or import, so file.insert_step's
`matrix` and `name` were dropped and the part landed at the origin, and
file.insert_mesh lost all its options. It keeps every parameter but the
path, as given; each command checks them as it does when run directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:40 +00:00
dudedagger42 84cad43154 Timeline marker: refuse a negative position, and name the command asked for (#4712) (#4749)
timeline.rollback / timeline.roll_to read a negative position as 0, so
an agent passing -1 for "one step back" rolled the whole timeline back
and every body disappeared, with no error. A negative position is now
refused. The two commands shared one function that refused in
roll_to's name; each now refuses in its own.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:38 +00:00
rlartot 5c280b9a94 fr: Répétition instead of Réseau for patterns (#4751)
Nine messages: the pattern tools, their headings and two sample
descriptions. "Réseau" can be mistaken for a network.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 13:44:36 +00:00
DuBearte 1a953736ff Combine takes bodies clicked in the Browser (#4452) (#4766)
* claim: #4452

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

* Combine takes bodies clicked in the Browser (#4452)

A body row in the Browser only replaced the selection, so with Combine
open its target and tools stayed empty and overlapping bodies could only
be picked in the view. When the open dialog's active input takes bodies,
a body row now goes there, as plane and axis rows already do.

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

* Combine Browser picks: test both layouts and control-channel clicks (#4452)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:32 +00:00
DuBearte 4106332738 Shortcuts: a sketch tool and a model command can share a key (#4163) (#4781)
The Keyboard Shortcuts page refused a key another command used, even a
sketch tool and a model command that never run in the same mode, so
Shift+S could not open Create Sketch in the model and place a point in a
sketch. Edited by hand, the press ran Create Sketch inside the sketch.

A sketch tool (SKETCH tab) and a model command (the other tabs) may now
share a key. A press runs the enabled command of the mode the view is in
first: the tool in a sketch, the model command outside. Two commands of
one mode still clash, and commands on no tab (undo, views, files) clash
with everything.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:29 +00:00
CamWight 9559ad398a Physical material: one rule for a body's material and its density (#4783) (#4785)
* claim: #4783

* Physical material: one rule for a body's material and its density (#4783)

inspect.measure, component.properties, occurrence.properties and the
Properties window took a body's material name from the raw assignment,
but its density from Document::density, which falls back to the default
material when the assigned one is unknown or has an unusable density. So
a damaged or hand-edited design read one material and weighed another: a
design material "Broken" at -3 g/cm³ read "Broken" at 1.29 g/cm³.

One rule, in one place:
- DEFAULT_MATERIAL (new, beside DEFAULT_DENSITY in the material library)
  is the default material's name. The library's first row uses it.
- Document::body_material (new, beside Document::density) is the material
  a body's mass is computed with, as the library or the design spells it:
  the assigned one when it is known and has a usable density, else
  DEFAULT_MATERIAL. Document::density is that material's density, with
  the same value as before for every input.

inspect.measure and the physical properties name body_material.
inspect.measure now names every body's material, measured or not, since
naming it takes no measuring; the density stays with the mass, on the
bodies measured. So the Properties window shows the engine's name without
a fallback of its own. The places that keep the assignment as stored
(configuration cells, material.assign, material.edit, material.delete
and the dialogs) only use the constant, so a configuration still keeps a
damaged assignment as it is.

The visible change: a body whose material is unknown, or has an unusable
density, reads "Default", the material it weighs, and a miscased name
reads as the library or the design spells it. A body inspect.measure
can't measure now has its material too. Masses and densities don't
change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:27 +00:00
DuBearte bddb52c96a Move: the value box follows the dragged axis, and Tab steps through X, Y, Z (#4776) (#4788)
The Move dialog's floating value box always showed and edited the X
distance, whichever arrow was dragged, and Tab left the box for another
widget. Now:
- dragging an arrow shows that axis's distance, turning a ring (Free Move)
  that axis's angle; a plane square or the ball leaves the box as it is;
- Tab in the box goes to the next value (X, Y, Z, then in Free Move the
  X, Y and Z angles) and Shift+Tab back, the keyboard staying in the box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:25 +00:00
raykholo a0fd39a7be Flange on a plain solid says how to start sheet metal (#2987) (#4789)
* claim: #2987

* Flange on a plain solid says how to start sheet metal (#2987)

Flange, Flat Pattern, and Export DXF on a design with no sheet metal body
now say how to make one: Flange > Base, or Convert to Sheet Metal on a
plate's large flat face. An edge flange checks for a sheet metal body
before it adds a feature. The message is in the catalogs, and engine
messages that match a catalog entry are shown in the interface language.

* Dialogs show a refusal without the engine's parameter prefix (#2987)

The Flange dialog showed "invalid parameters for `sheet.flange`: " in front
of the new guidance. The dialog already names its command, so the red
refusal text leaves that prefix out and follows the interface language
when the message is in the catalogs. Create Issue still gets the full text.
2026-10-23 13:44:24 +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
raykholo bd0cc4bb51 Units of new designs apply to the empty design already open (#221) (#4835)
* claim: #221

* Units of new designs apply to the blank design already open (#221)

Changing Preferences > Units of new designs only took effect at the next
File > New, so the empty design open at launch stayed in millimetres. When
the preference changes (the dialog, Cancel, Restore Defaults, or
preferences.set) and the open design is blank and unsaved, it takes the new
units at once, without an undo step or unsaved changes. A design with
content or a file keeps its units.
2026-10-23 13:44:18 +00:00
raykholo 4c8ae5d078 Hem: the direction arrow points the way the hem builds (#4794) (#4836)
* claim: #4794

* Hem: the direction arrow points the way the hem builds (#4794)

The Hem dialog shared the edge flange's arrow, which points out of the
sheet's larger neighbouring face. A hem folds back the other way, so the
arrow pointed away from the material it adds. Flip the hem's arrow so it
matches the geometry; edge flanges keep theirs.

A regression test compares the arrow with the side the finished hem adds
material on, for both Flip states, top and bottom edge picks, and both
dialog layouts.
2026-10-23 13:44:17 +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 e104e241a3 Rename a body: keep the names of siblings a later feature removed (#4880) (#4904)
* claim: #4880

* Rename a body: keep the names of siblings a later feature removed (#4880)

Renaming a body sets the stored names of all bodies its feature made, so
the siblings are not renumbered. Those names were read from the final
model, where a body that a later feature removed no longer exists: its
stored name was lost, it came back as `Ext (3)`, and the later Remove
failed with "no body `Body3`". Read the siblings from the feature's own
result instead, as the later features already are.

* Test: renaming one half of a split leaves the other

Renaming one half keeps the other half's name, also once a later feature
joins that other half away, after both an incremental and a full
evaluation.
2026-10-23 13:44:09 +00:00
raykholo 3bca787213 MCP tools: unknown command ids suggest the closest ids (#4846) (#4915)
* claim: #4846

* MCP tools: unknown command ids suggest the closest ids (#4846)

An unknown command id in `execute` or `batch` now lists up to three closest
ids, and an id with parameters packed into it says to pass them in `params`.
2026-10-23 13:44:07 +00:00
raykholo 21257c39c2 Keyboard Shortcuts page: draw only the visible rows (#4804) (#4919)
* claim: #4804

* Keyboard Shortcuts page: draw only the visible rows (#4804)

Off-screen rows keep their height but skip their cells, the list is
filtered once before measuring and drawing, and each row's shortcut
comes from the command spec already in hand instead of a registry
lookup.
2026-10-23 13:44:06 +00:00
raykholo ed6e123d54 Sketch: offer Break Offset Association for offsets of lines (#4899) (#4920)
* claim: #4899

* Sketch: offer Break Offset Association for offsets of lines (#4899)

Trim and Extend refuse an associated offset output and point to Break
Offset Association, but the right-click menu offered it only for
free-form offsets. Offer it whenever the curve is an offset output
(analytic, topology or free-form), using the sketch's shared lookup.
2026-10-23 13:44:04 +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
Jasper van Loenen d1f5d6e223 History: drag features across the marker in one step, clearer drop feedback (#4938)
Dropping a feature just behind the history marker now rolls it back, and
dropping it just before the marker makes it active again, in both the
bottom strip and the left panel. timeline.reorder and timeline.move_many
take an optional `active` param that sets the marker in the same command,
so the move is one undo step and one recompute. A position on the wrong
side of the marker for the requested `active` is refused.

Drop feedback:
- bottom strip: the drop line sits centred between two features (it
  used to touch the previous one) and shows at the first position
  (it used to be drawn outside the strip there)
- left panel: a drop slot below the marker when it is at the end
- no drop line when the marker is dragged back to where it is
- a dragged feature is outlined in the drop line's green
- grab cursor on the marker, grabbing cursor while dragging the marker
  or a feature

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:00 +00:00
Logan Night 1b215b521d icon_audit: compare the requested-icons manifest with CRLF checkouts ignored (#4932) (#4960)
* claim: #4932

* icon_audit: compare the requested-icons manifest with CRLF checkouts ignored (#4932)

On Windows, Git for Windows checks out requested_icons.txt with CRLF line
endings (core.autocrlf; the repo has no .gitattributes), while the test
builds the manifest with \n, so the comparison fails although the lists
are the same. Read the file with \r\n normalised to \n, as #1931 did for
the icon tools. Regenerating still writes \n.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:56 +00:00
raykholo 36a354e3a7 Agent chat: serve the bundled skills' reference through an MCP tool (#4966) (#4970)
* claim: #4966

* Agent chat: serve the bundled skills' reference through an MCP tool (#4966)

The bundled skills start with "read reference.md", but SolidCraft's own
chat turns the agent's file tools off, so its agent could not follow them.

The MCP server gains a read-only `skill_reference` tool that returns a
skill's documents from copies built into the binary. It takes a skill
name and a file name from a fixed list, never a path, and refuses an
unknown skill or file with the available ones. The skills point to it,
and docs/mcp.md and skills/README.md describe it.
2026-10-23 13:43:54 +00:00
Taha Zarif a05a0f1053 Fix/mcp stdio bounded request lines (#4972)
* test(mcp): reproduce whitespace bypass of request-size limit

Add a source-level regression that rejects a JSON-RPC request larger than
MAX_LINE even when most of it is whitespace, plus bounded-stdio framing
coverage and invalid UTF-8 recovery.

* fix(mcp): bound stdio line buffering and validate raw request size

Consume oversize JSON-RPC lines in bounded memory while keeping the
following request available. Reject whitespace-padded lines before
trimming, preventing a request-size cap bypass.

* docs(mcp): keep server framing comment idiomatic
2026-10-23 13:43:52 +00:00
HiroKenEz 60efbe39e3 Sketch drag: a linkage with redundant parallelogram constraints still moves (#4585) (#4983)
* claim: #4585

* Sketch drag: a linkage held by redundant parallelograms still moves (#4585)

Cursor motion looks for the sketch's freedom in the null space of the
constraint Jacobian. Redundant constraints (a parallelogram held both
parallel and equal on each pair of sides) are dependent only at an exact
solution; after a correction to the solver's tolerance their rows differ
by about 1e-9, which the 1e-9 pivot tolerance counted as rank, so the
null space came out empty and a one-DoF linkage stayed locked. Use the
rank analysis' 1e-7, so the drag sees the freedom the sketch reports.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:49 +00:00
raykholo 9e20a373ca Browser: Shift+click above the anchor selects the whole range (#4991) (#4997)
* claim: #4991

* Browser: Shift+click above the anchor selects the whole range (#4991)

The browser rebuilds its row order while drawing, and a Shift click looked
the anchor up in that half-built list, so an anchor further down was not
there yet and only the clicked row was selected. Resolve the range against
the previous frame's complete row order instead.
2026-10-23 13:43:47 +00:00
Daniel 2f55a39c9c Timeline: delete the selected features, Shift+click ranges, delete after the history marker (#5005) (#5006)
* claim: #5005

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

* Timeline: delete the selected features, Shift+click ranges, delete after the history marker (#5005)

Delete in a feature's menu deletes the selected features when the
right-clicked one is among them ("Delete 3 Selected Features").
Shift+click selects the range from the last feature clicked (or the last
selected one), in the order taken before drawing. The menu of a feature
after the history marker deletes every feature after the marker, with
their number in the label. All of it goes through delete_features: one
timeline.delete and one undo step. The confirmation no longer lists
features that are themselves being deleted.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:45 +00:00
Jackson Oursland 80fd2fbbf1 Assistant: add Anthropic-compatible API agents (#5015) (#5017)
* claim: #5015

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

* Assistant: add Anthropic-compatible API agents (#5015)

The Add agent wizard now offers Anthropic-compatible endpoints beside OpenAI-compatible ones. The API backend speaks the Anthropic Messages protocol with the same MCP bridge, tool gate, limits, undo and keychain rules: x-api-key and anthropic-version, a top-level system note, input_schema tools, content-block answers and one user tool_result message per round. Test sends a short message because compatible endpoints (Qwen's) need not list models.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 13:43:41 +00:00
raykholo c3d1ce8237 Opening a design: range-check integer-valued coordinates too (#5021) (#5026)
* claim: #5021

* Opening a design: range-check integer-valued coordinates too (#5021)

The file-level number check skips every integer-valued number so that ids
and counters may be large, which let a coordinate such as
9223372036854775808 through. The typed design is now checked as well:
float fields (coordinates, sizes) are serialized as floats and must be
within the same +-1e9 limit, while integer fields stay integers and are
not limited.
2026-10-23 13:43:40 +00:00
raykholo 567bc50723 Snap fit: refuse sizes too small to build (#5022) (#5027)
* claim: #5022

* Plastic features: refuse sizes under 0.01 mm (#5022)

A snap fit with a near-zero thickness (1e-9 mm) got past the size checks,
which only asked for a positive number, and panicked in the kernel's
intersection code. The boss, lip, groove and rest had the same gap.

Every length of a plastic feature that becomes geometry (snap fit length,
thickness, width, catch_depth and catch_length; boss diameter, height,
hole_diameter, hole_depth and rib sizes; lip and groove width and height;
rest width, length, height and thickness) must now be at least 0.01 mm.
The check sits where the feature is evaluated, so a parameter changed later
is refused the same way, and the message names the parameter.
2026-10-23 13:43:38 +00:00
raykholo 1a5fcd05d6 Extrude and loft: refuse duplicate profile indexes (#5023) (#5028)
* claim: #5023

* Extrude and loft: refuse repeated profile indexes (#5023)

`profiles: [0, 0, ...]` with 10 000 copies built 10 000 identical bodies in
about 35 s, and a loft section listing one profile 10 000 times took 25 s
before it was refused.

A list of profile indexes now refuses a repeated index while it is parsed
("profile 0 is listed twice"), as offset association ids already were.
Before the profiles are resolved, a selection naming more profiles than its
sketch has, an index the sketch doesn't have, or two points in the same
profile is refused too. A design file that repeats an index still opens,
using the profile once.
2026-10-23 13:43:36 +00:00
Jackson Oursland 3a6153d18f Add .vscode to .gitignore. (#5588) 2026-10-23 12:49:05 -04:00
Cyryl Sochacki d273c45de6 i18n(pl): translate 213 missing messages (#5031)
Catch up with en.json: the new Preferences pages, assistant settings,
3D mouse axis assignment, bug report form, history names and the tool
help texts. pl now covers every message of en.json.
2026-10-23 13:43:32 +00:00
dudedagger42 da46c660df Coincident on two circles lays the second on the first (#5038)
Coincident with two circles (or arcs) picked was refused: "two curves must both
be straight lines; otherwise choose a point and a curve". It now lays the second
pick on the first: Concentric and Equal together, in one step, with the first
staying put. A pair already concentric or equal gets only the other half; one
already lying on the other is refused as nothing to add.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:30 +00:00
vdirienzo 92d1331978 A renamed body keeps its number: the next body does not take the freed name (#4957) (#5053)
* A renamed body keeps its number (#4957)

The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.

The design now saves the names a rename freed, and the counter skips
them.

* A renamed body keeps its number (#4957)

The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.

The design now saves the names a rename freed, and the counter skips
them.

* Format
2026-10-23 13:43:28 +00:00
dudedagger42 01bbc0aa9f Parameters: a favourite renames by double-click (#4952) (#5070)
A favourite is listed twice, under Favourites and under User. Double-clicking its name opened a
name editor in both rows, under one widget id; the two took each other's focus, one lost it at
once, and the rename closed with the name unchanged. The rename now opens only in the table it
was started in, under an id that includes that table.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:26 +00:00
raykholo dbbad6d4ee view.apply: keep the camera's roll, and refuse an up axis (#5087) (#5094)
* claim: #5087

* view.apply: keep the camera's roll, and refuse an up axis (#5087)

An explicit camera was read as a named view, which has no roll or up, so
roll was silently reset to 0 and up was ignored. Read roll as a finite
number in radians, clamped to ±π, and refuse up with a message saying the
up axis comes from the design settings.
2026-10-23 13:43:24 +00:00
raykholo 4f69e6540c Dragging a docked dialog no longer highlights the view under it (#5086) (#5095)
A docked dialog is placed from the previous frame's drag offset, so a fast
drag can leave the pointer outside the dialog, over the model. The viewport
then treated the pointer as hovering the model and highlighted faces and
edges under it.

drag_dock now marks the frame in egui temp data while its title band is
dragged, and navigation::handle treats the pointer as not hovering the view
while that mark is current and a button is held. This covers every dialog
docked through drag_dock.
2026-10-23 13:43:23 +00:00
ivisaf 55608665ef gpu: open the OpenGL regression device without indirect-call validation (#5101)
NVIDIA's EGL answers wgpu's request for a GL 3.3 context with exactly 3.3
(GLSL 3.30) yet lists GL_ARB_compute_shader. wgpu-hal then reports compute
shaders, wgpu-core builds its indirect-call validation pipeline, naga cannot
emit compute GLSL below 4.30, and request_device fails as Device(Lost).
The test draws nothing indirect, so it opts out of that validation.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:20 +00:00
raykholo 0fb4a3e868 GPU tessellation: an empty plan no longer fails buffer validation (#5049) (#5104)
* claim: #5049

* GPU tessellation: an empty plan no longer fails buffer validation (#5049)

Empty tessellation inputs were padded to 16 bytes, smaller than the
shader's 32-byte vertex and 48-byte surface records, so wgpu refused the
dispatch. Skip the dispatch when there is nothing to evaluate, and pad
each empty buffer to one whole element of what the shader reads.
2026-10-23 13:43:18 +00:00
Christian Hammacher 2dc31a30d4 Extract to File: move a component into its own linked design (#5113)
The component's sketches, features, bodies, subcomponents, the joints among
them and the parameters they use move to a new .solidcraft design, keeping
their ids and names, and the component becomes linked to it as Insert Design
makes it. The new design must build the same bodies and the assembly must
look the same afterwards, else nothing changes and no file is written. Joint
faces on the component are found again on the linked bodies, and components
that build the same bodies can become placements of the link.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:16 +00:00
sthifen 8b68eb7eac fix(extrude): a sketch on an inserted part joins and cuts it, through all too (#5115)
A STEP file inserted into a new design sits in its own component under the
empty root, and a sketch on its face must join that part going out and cut it
going in rather than make a separate body beside it. With a distance, main
already resolves this; Through All with no distance gave `auto` no reach to
probe, so it fell back to New Body and cut nothing. Through All now reaches
past every body in its direction, and `auto` reads it as a cut (#1234).

Co-authored-by: Steven <steven2021067613@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:14 +00:00
bherbruck 96679f3e2b Themes: drop the logo's fourth colour, which #5465 retired (#5548)
The 14 new built-in themes still listed four logo colours, so none of them read after #5465 and main's theme test failed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:39:30 -04:00
froglemonade2 3ba17bb517 App icon: draw the borderless mark everywhere (#5461) (#5465)
* claim: #5461

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

* App icon: draw the borderless mark everywhere (#5461)

app_icon.rs drew the older mark with white lines between the faces and a
white outline around it, so the Windows, Linux, web and README icons did
not match the macOS icon, the social card or the title bar. It now draws
the borderless mark from the faces and gradients of the macOS artwork
(03-solid-blue-cutout.svg): five faces with transparent gaps between them.
The filler fills even-odd spans so the concave faces work.

The Linux hicolor PNGs and both Windows .ico files are regenerated with
`cargo xtask app-icons`. The README logo, the web app's icons and every
package pick them up from there.

The logo colour token's fourth entry (the white edge) is gone: nothing
draws it any more.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:40:09 -04:00
N3MI-DG ae3b36ac88 Agent cursor follows the agent's own pointer input (ui.move, ui.click, ui.drag, ui.scroll) (#5162)
The agent cursor only acted out engine commands: pointer input sent over the control channel
reached the app as synthetic events without moving it, so an agent's clicks, right-clicks and
drags were invisible to a person watching. While the cursor is shown, each synthetic pointer event
now moves it to where the input lands, a press leaves a ripple, and a drag is followed step by
step. A request being acted out keeps the pointer; with the cursor hidden nothing changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:42:59 +00:00
raykholo 5367ef4367 Extrude without a profile selection keeps holes in nested loops (#5172) (#5176)
* claim: #5172

* Extrude without profiles keeps nested loops as holes (#5172)

With no `profiles`, solid.extrude took every region of the sketch, so
circles drawn inside a plate outline were extruded as well and filled the
plate's holes. The default now applies even-odd nesting to the drawn
profiles: a loop inside an outline is its hole, a loop inside that hole is
material again, and separate outlines all extrude. Sketches without nesting
keep the `all` selection, and explicit `profiles` (including "all") are
unchanged. Saved designs store their selection, so they recompute as before.
2026-10-23 13:42:55 +00:00
raykholo caa1e6e196 component.create refuses parameters it does not take (#5174) (#5178)
* claim: #5174

* component.create refuses parameters it does not take (#5174)

A call with `bodies` (or a misspelled key) used to succeed and make an empty
component. Unknown keys are now refused before anything changes, the error
lists the accepted parameters, and `bodies` points to component.move_bodies
and component.from_bodies.
2026-10-23 13:42:53 +00:00
raykholo 8bd17cea42 Joints: a joint origin's name given as a face says to pass it as the origin (#5175) (#5179)
* claim: #5175

* Joints: a joint origin's name given as a face says to pass it as the origin (#5175)

When the name in `face` or `circle` matches no face but is a joint origin,
the error now says so instead of "no face is named".
2026-10-23 13:42:51 +00:00
takuto-NA 4256775274 Joint: avoid redundant analysis and residual placement work (#5185) (#5186)
* claim: #5185

* Joint: reuse residual placements and skip discarded mobility reports (#5185)
2026-10-23 13:42:49 +00: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
github-actions[bot] b6a45b8278 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 14:19:26 +00:00
N3MI-DG 8ae90f9667 Home: a recent design's location shows in one tooltip, not two (#5208)
The File view's recent row already shows its path on hover; Home added
a second hover text with the raw path, so both appeared.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:42:43 +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
raykholo 8b1a722f06 Viewport: the ground shadow no longer sends every frame through the transparency path (#5230) (#5255)
* claim: #5230

* Viewport: draw the ground shadow on the direct path (#5230)

The ground shadow went into the model's translucent triangles, so every
frame with a body counted as see-through and took the order-independent
transparency path. Give it its own batch, drawn alpha-blended after the
opaque geometry as before, so only truly see-through bodies use that path.
2026-10-23 13:42:38 +00:00
takuto-NA 63d81cc096 Motion Trace: regenerate kept curves only when inputs change (#5248) (#5270)
* claim: #5248

* feat(motion): regenerate kept traces when source inputs change

* feat(motion): add a parametric Chebyshev trace demo

* perf(joints): reuse face measurements across motion samples

Face-centred joint origins called Body::faces on every solve, bypassing the
reference-mesh reuse used for rigid placements. Share face measurements with
unchanged model geometry at the existing tolerance, while guarding replaced
bodies and applying offsets after the cached measurements.

Cover repeated named/nearest-face solves, shape edits, retained snapshots and
direct replacement with deterministic work counts. The 145-point Chebyshev
dimension and range updates retain identical sampled and interpolated curves.

* perf(naming): skip preparation for cached face names

Both face lookup paths spawned fresh workers over every body even when their name cells were ready. Prepare only cold cells, with direct handling of zero or one cold body. Preserve the existing parallel naming path and all face selection semantics.

Deterministic regressions cover repeated picked and unique-name lookups, misses, partial warmup, renames, snapshots and moved copies. Release Chebyshev medians fall from 859 to 50 ms for a dimension edit and 871 to 34 ms for a sweep edit; all 145 samples and interpolated points remain identical. Full cargo xtask ci passes with RUST_TEST_THREADS=4 and the existing local Windows validation overlays.
2026-10-23 13:42:36 +00:00
raykholo 46a8f86e80 web-offline-test: wait only for the files the browser can ask for at once (#5287) (#5294)
* claim: #5287

* web-offline-test: wait only for the files the browser can ask for at once

The fail-fast step waited for every other file of build v2 to reach the
server. Chrome sends at most six requests to one server at a time, and
the page's failure cancels the download, with it the files still waiting
for a connection. A real build keeps nine files, so only six were ever
asked for and the Pages job failed with "only 6 of 8 slow files were
asked for" whenever the installing worker didn't happen to ask again.

Wait for as many as can be in flight (less the page's connection), and
give the stand-in app more files than that, so the default run covers it.
2026-10-23 13:42:33 +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 867df396ea Agent chat in the Flatpak says why no agent is found and how to allow one (#4765) (#4769)
* claim: #4765

* Agent chat: explain the Flatpak sandbox when no agent is found (#4765)

In the Flatpak the sandbox has no home-folder or network access, so agents
installed through nvm or ~/.local/bin are never discovered and the chat only
said that no agent was found. With FLATPAK_ID set and nothing discovered, the
picker and Preferences > AI now say why and offer the exact
`flatpak override --user ai.storyteller.solidcraft ...` command with a Copy
button. The message is in every catalog, and docs/agent-chat.md gains a
Flatpak section listing what each permission is for. The manifest is
unchanged.
2026-10-23 13:42:14 +00:00
JEREMY 858e5995d6 Plane at Angle: a planar face is a base plane (#1939) (#1956)
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
2026-10-23 11:32:24 +00:00
bherbruck edf57cec29 Stand up Y-up STEP and IGES files from SolidWorks automatically (#5191)
* claim: #5150

* Stand it up: don't offer it for SolidCraft's own STEP files (#5150)

STEP replies from file.insert_step and doc.open now carry the file's
originating system, and the window skips the stand-up offer when it is
exactly SolidCraft, as SolidCraft's STEP export writes it.

* Stand up Y-up STEP and IGES files from SolidWorks automatically

The STEP header (originating system, preprocessor version, FILE_DESCRIPTION) and the IGES
global section's native system ID say which program wrote a file. A SolidWorks header means
Y up: the import's component is stood up as it opens, as one undo step, and the message offers
Undo. SolidCraft's own files are left alone; an unknown exporter keeps the offer.

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

---------

Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 02:53:20 +00:00
raykholo 0c72ad56d2 An idle window draws nothing with the frame-rate readout on (#5165)
The viewport's frame-rate readout asked for a new frame every frame, so with
it on an untouched window redrew as fast as the display allowed and kept the
GPU busy: 77% GPU at 60 fps on an M1 with a 5K display showing the sample,
against 0% with the readout off. It now asks for no frames of its own and
shows how often the view really draws; one frame a little over a second after
the frames stop shows the view idle at 0 FPS. The preference's note no longer
says the view redraws continuously.

The status toast also asked for a frame every half second for the six
seconds it shows; it now asks for one frame, when it is due to go.
2026-10-23 02:19:37 +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
Jackson Oursland b0048a6104 Mesh bodies: measure their size once, when made (#4662) (#4663)
`Body::size` walked a mesh body's vertices on every call, and it is asked
for every frame. A mesh body's triangles never change after `mesh_body` or
`mesh_transformed` makes it, so both now measure it then (`TriMesh::span`)
and `size` returns it like a B-rep body's. `ModelBody::mesh` returns a mesh
it already has before working out the display tolerance.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:51:40 -04:00
raykholo 0db2abbeca Viewport legend and readouts stay above the floating History card (#4107) (#4129)
* claim: #4107

* Keep bottom viewport overlays above the floating History card (#4107)

In the floating layout the History card covered the analysis legend and
the frame-rate readout (#3896). One bottom inset, computed from the
History card (zero when History is hidden or docked), now positions every
bottom-anchored viewport overlay: the surface-analysis and Continuity
legends, the frame-rate readout, the command-progress chip and the
navigation bar fallback.
2026-10-23 00:48:27 +00:00
bherbruck 9761d75699 Release: a failed snap build no longer holds up the release (#5168)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:50:38 -04:00
bherbruck a06bdf5cfc Release 0.20.0
The tree is main at 030187452 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-22 18:34:40 -04:00
bherbruck 030187452a Release 0.20.0 (#5118)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:40:01 -04:00
ralfkuh-lab 198fbeb814 i18n: every UI label reaches the catalog, on the UI kit (#2483)
On main, 120 label literals (95 distinct) can't be translated in any
locale: the English catalog doesn't reach them. Catalogue them in en,
ja, zh and de, translate indented row labels once for display and
measuring, look up the thread profile by its id, and give each
navigation preset's help, shown by the mouse mapping picker, a catalog
message.

A guard tokenizes the UI sources and checks every label literal of a
kit call, of context_menu::button and of tr_label/tr/SelInput against
the English catalog, resolving kit calls by their imports.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:08:05 +00:00
Adam Hickerson 186096cbe6 agent chat: agent detection finishes on the start page (#3265) 2026-10-22 20:52:13 +00:00
Colin Faulkingham 3b982ca371 Prevent deleted item IDs from being reused (#1016) (#3641)
* claim: #1016

* Prevent deleted design item IDs from being reused
2026-10-22 18:31:39 +00:00
Loohi aaa110218b Edit Offset Plane: keep a face base, refuse a plane on itself (#3567) (#3889)
* claim: #3567

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

* Edit Offset Plane: keep a face base, refuse a plane on itself (#3567)

Reopening an Offset Plane built on a face looked the face up at the
plane's origin, which can lie off the face, so the Plane input came up
empty. Look it up at the point picked on the face. A redefine that
builds a feature on itself is refused with a plain message instead of
failing with "plane offsets nested too deeply".

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:30:02 +00:00
raykholo 46f80b769f Sketch fillet and chamfer refuse an offset's corners instead of leaving circles (#2874) (#3860)
* claim: #2874

* Sketch fillet and chamfer refuse an offset's corners (#2874)

An analytic offset's output ends are recomputed from its source curves on
every solve and held fixed there. Filleting an output corner moved those
ends to the tangent points, the next refresh pulled them back to the sharp
corner, and the new arcs closed into full circles with the sketch left
unsolved.

Fillet and chamfer now refuse a corner on an offset output before anything
changes (a batch fillet applies all of its corners or none) and say how to
get the rounded result: round the source corners before offsetting, or
break the offset association first, after which the same corners round
normally.
2026-10-22 18:21:13 +00:00
raykholo 26ca82735b Circular Pattern and Revolve: a construction axis can be the axis (#2885, #4506) (#3796)
* claim: #2885

* Circular Pattern and Revolve: a construction axis can be the axis (#2885)

The dialogs send a picked axis by its name, but solid.pattern.circular
only knew X, Y and Z, and solid.revolve / surface.revolve read any other
name as a sketch line id. Picking a construction axis therefore failed.

- solid.pattern.circular takes a construction axis name, saves it as
  `axis_feature` and turns about the axis as evaluated, so the copies
  follow it when it is edited.
- AxisRef::Construction { name } for revolve and surface revolve,
  resolved at evaluation and checked to lie in the sketch plane.
- construct.plane.at_angle accepts a construction axis name (its fixed
  axis line is taken from the axis as it is now).
- Reopening these features shows the construction axis as the pick.
- Design format: pending/construction-axis-refs.rs.

* Construction axis references follow the axis in the feature's component frame (#4506)

- Revolve, Surface Revolve and Circular Pattern resolve a construction axis
  where it is evaluated, mapped into the feature's component frame.
- A line of the revolved sketch with the same id as a construction axis
  still wins, so existing scripts keep their meaning.
- Tests: the revolve keeps the reference through save and reopen and
  follows an edited axis (60π to 80π mm³); perpendicular, offset and
  profile-crossing axes are refused without changing the design.
2026-10-22 18:21:11 +00:00
SS-EN 408d30ea56 Joint drive moves parts without evaluating them (#3391) (#3397)
* Joint drive moves parts without evaluating them

A joint drive (also the Drive dialog and motion steps) changes only occurrence transforms, yet it
re-evaluated nearly the whole timeline: every feature that might read another component's
geometry (a sketch with any link, even to its own axis) hashed every occurrence's transform into
its fingerprint, so any motion invalidated it and everything after it, and no new pose was ever
in the cache.

- Placement digest: a feature now hashes only where the geometry it actually reads lies relative
  to its own frame (link sources, anchors, profiles and paths from other components, mirror
  placements; To Object and face/edge-picked planes read every component, relatively). Moving
  parts that keep those relative placements leaves every result in place: nothing is evaluated
  or re-meshed. In-context features whose referenced part really moved still rebuild.
- Evaluated ahead, applied once: `Ahead::evaluate` returns what it evaluated and
  `Session::prime` lets the window-thread pass take it (history commands and dialog OK), so an
  off-thread command no longer costs twice on designs past the cache's budget.
- Contact meshes are kept per body geometry across drives.

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

* Joint drive: booleans read only their input bodies' placements

A join, cut or intersect with named targets, and a Combine, read every
placement, so a joined part transferred to a component re-ran its boolean
at each new joint pose. Each now hashes only the placements of the bodies
it actually takes as inputs, resolved in its timeline input state (before
any later BodyTransfer), relative to the frame that evaluates it; Combine
reads its tools relative to its target. Unresolved and legacy component or
path reads keep the conservative dependency. 24 new joint_pose_ tests
cover new angles, transfers, targets, save/reopen, undo and real relative
motion (900 -> 800 mm3).

Supplement offered on #3397 by takuto-NA.

Co-Authored-By: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Creating a component recomputes nothing

A new, empty component changes nothing a feature reads, yet it changed
the placement fingerprint of every targeted boolean, Combine and To
Object feature, so `component.create` evaluated the timeline again:
- the first component of a design: `placement_digest` returned 0 while
  the design had no placements and a nonzero value once it had one;
- To / From Object and legacy face- or edge-picked planes hashed every
  component, not just those holding the bodies, sketches and
  construction geometry the feature can reach.

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

* Test: creating a component after a legacy face sketch recomputes nothing

A face extruded after its construction certificate is gone (a patterned
cut, a pad joined flush with it, then one more cut) is sketched on a
legacy face plane, which reads every placed body. A new, empty component
must not change what it reads either (reported by Tyler Chiriboga).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
2026-10-22 18:21:08 +00:00
Jackson Oursland 75261cd797 Viewport: order-independent transparency for see-through surfaces (#4734) (#4903)
* Viewport: levels of detail for large meshes (#4724)

A large instanced mesh (20 k triangles or more) gets coarser copies, made
once on a worker (`mesh_lod`): a quarter, a sixteenth and a sixty-fourth of
its triangles, by quadric edge collapse (`geom::simplify`, after Garland and
Heckbert 1997; no fold-overs, no sheets joined, open boundaries kept), each
with its measured deviation from the full mesh. Each frame, as placements
are culled, each is drawn from the coarsest copy whose deviation stays
under 0.75 px where it stands (`placement_scale`, `level_for`). Until the
copies are made the mesh is drawn in full; the scene is built again when
they are. Display only: picking, measuring and exports keep the reference
mesh. Outside the kernel.

25 placements of a 2 M-triangle STL, 3600 x 2190 px, 4x MSAA, M2 Max
(`instanced_scene_gpu_benchmark`): 20.2 -> 1.2 ms a frame with all in
view, 21.9 -> 2.9 ms close up. The copies take 10 s to make, on a worker.

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

* Viewport: see-through and faded parts drawn as instances (#4888)

See-through (an appearance's opacity) and faded (context around an active
component) bodies in one colour are drawn as instances like opaque ones,
their alpha in each placement's colour: one copy of the triangles, levels
of detail and culling, no per-placement copies built on the window thread.
Their placements are ordered back to front each frame (not their
triangles one by one) for devices that sort see-through surfaces.

The GPU benchmark gains `SOLIDCRAFT_BENCH_OPACITY` and the CPU share of
each frame, and its pictures each placement's own triangles too.

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

* Viewport: order-independent transparency for see-through surfaces (#4734)

The model's see-through surfaces (glass, faded context; instanced or not)
were sorted back to front triangle by triangle on the window thread
whenever the view turned. They are now drawn with weighted blended order-
independent transparency (McGuire and Bavoil 2013): when there are any,
the frame is drawn into the renderer's own textures (as supersampling
does), the see-through surfaces gathered into a weighted colour and a
revealage target against the frame's depth, laid over the frame in one
full-screen pass, then the overlays drawn over them. Nothing is sorted.
A device that cannot blend into float targets keeps the sorted way.

25 placements of a 2 M-triangle STL at opacity 0.5, M2 Max, 3600 x 2190 px:
about 10 s a frame sorted, 21.6 ms (17.2 ms instanced, 2.8 ms with levels
of detail).

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 16:17:39 +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
bherbruck 1814adb06a Leaving a sketch keeps the camera where it is (#4909)
Finish Sketch, double Esc (including a blank sketch discarded), and Cancel no longer
restore the pre-sketch view. Entering a sketch still turns the view to face the plane.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 11:04:04 -04:00
Nathan Dane 971b28c982 claim: preferences redesign (#4608) 2026-10-22 13:53:36 +00:00
Nathan Dane f79cbc216c Redesign the home page as the File view (#2027) (#3822)
* claim: #2027

* Redesign the home page as the File view (#2027)

* Match the File view with the accepted component design (#2027)

* Restore the Start subtitle and simplify the recovery icon (#2027)

* Integrate File view with main's shared UI containers
2026-10-22 12:19:09 +00:00
Jackson Oursland 4dc1828138 Imported meshes: share their triangles between document copies (#4673) (#4675)
Every edit clones the document for undo, and `MeshData`'s positions and
triangles (plain `Vec`s) were copied with it: ~165 MB a command for a 2 M-
triangle STL. They are now `Arc<[f64]>` and `Arc<[u32]>`, shared by clones;
the few places that rewrite them build new arrays. Saved designs are
unchanged (an `Arc<[T]>` serializes as the array).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:06:44 +00:00
Jackson Oursland f843a3f83c Picking: remove the GPU pick buffer, the per-body BVHs answer alone (#4665) (#4666)
The pick buffer drew the model's surfaces and edge ids for every still
view to narrow which edges a pick tests. Measured against the BVHs alone
(release, M2 Max, 1400 x 900 view, 600 cursor positions), a pick costs
2-51 us through the BVHs and the tile saved 0-4 us of it, while each pass
cost 0.7-12.5 ms of GPU time per still view, plus gathering its scene on
the window thread, read-backs a frame or two late and repaints to wait for
them (and, before, a copy of every triangle, which made it refuse models
past 6 M triangles). Picks are unchanged: the BVHs give the same answers.

`pick_timing_on_curved_models` gains eight layers of bodies seen from above
(the pick buffer's best case) and `SOLIDCRAFT_BENCH_MESH` placements.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:37 +00:00
Jackson Oursland f0fd5c10b7 Viewport: keep a face's hover highlight while the pointer moves across it (#4668) (#4669)
The highlight's key hashed the hovered item with its point under the
pointer, so every move across a face built and uploaded its highlight
again: for an imported mesh (one face) all its triangles, every frame.
The key now leaves out a face's and a plane's point, which the highlight
does not draw; an edge's middle stays in (it picks the part lit).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:33 +00:00
Jackson Oursland 981d23d1d2 Viewport: weld a large mesh's shared vertices quicker, and ahead of its first frame (#4679) (#4680)
`SharedTris::new` used the standard SipHash hasher and collected each
triangle's corners into a Vec: 451 ms for a 2 M-triangle STL on the window
thread. A quick hasher for the coordinates' bits and a fixed array make it
278 ms, and the background mesher that meshes an opened or inserted
file's bodies now welds their large meshes too (`warm_shared_tris`), so the
first scene that shows them takes the shared vertices ready-made.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:28 +00:00
Jackson Oursland 01648da72d Ribbon: work out each tab's panel lists once (#4658) (#4659)
`toolbar_custom::panel_lists` is asked for every frame and matched every
registered command against every section by string each time (~337 us a
call for MODEL, several calls a frame). The registry and the ribbon layout
never change while the app runs, so each tab's lists are kept: 0.2 us a call.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:24 +00:00
Jackson Oursland 3d3dd27439 Viewport: leave out placements of an instanced mesh that are out of view (#4646) (#4647)
* claim: #4616

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

* Viewport: draw a part placed many times from one copy of its triangles (#4616)

A part placed by many occurrences was drawn, framed and picked from a moved
copy of its mesh per placement: on the CPU (the moved mesh and its scene
bytes) and on the GPU (its vertices uploaded once per placement).

- The model scene draws bodies that share a mesh as GPU instances: the
  shared triangles once, a rigid move and colour per placement
  (`InstancedMesh`, `vs_tri_inst`), in the solid, section and AO passes.
  `GpuScene::merged` expands them, so scene tests see world triangles.
- `ModelBody::mesh_in_place`, `plan_mesh_in_place_at`, `mesh_bounds` and
  `display_identity` give the shared mesh, its placement, the exact box and
  an identity without making the moved copy.
- Display refinement's body ids, Fit/Home bounds, the viewport's body boxes,
  the background mesher and picking use them (picking moves the ray into
  the part's frame and shares one pick index).

25 placements of a 2 M-triangle STL on an M2 Max: orbit 45 -> 37 ms a
frame, footprint 41 -> 16 GB, 24 copies 39 -> 18 s.

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

* Viewport: draw large meshes from shared, indexed vertices (#4637)

An instanced mesh's vertices are shared between its triangles and drawn
indexed (`SharedTris`). A flat-faceted mesh (an STL's facets) shares them by
position alone and lights each fragment with its triangle's own plane
(`vs_tri_inst_flat`, `fs_*_flat`), taken from the fragment's position
relative to the eye so it stays exact at any zoom; its vertex outputs are
kept few, since a tiled GPU stores every vertex's outputs. A body alone is
drawn this way from 50 k triangles. A mesh too large for one buffer falls
back to unshared vertices.

`instanced_scene_gpu_benchmark` (manual, ignored): 25 placements of a 2 M-
triangle STL at 3600 x 2190 px, 4x MSAA, M2 Max: 21.3 ms a frame for the
placements' own triangles, 16.1 ms shared and indexed.

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

* Viewport: leave out placements of an instanced mesh that are out of view (#4646)

Each frame `Batches::cull` keeps the placements whose box may be seen
(`placed_box_in_view`: all eight corners past one side of the view in clip
space, so it holds behind the eye too) and writes them again only when
that set changes. The manual benchmark gains `SOLIDCRAFT_BENCH_CLOSE=1`:
close up on the middle of 25 placements of a 2 M-triangle STL (13 in
view), 3600 x 2190 px, 4x MSAA, M2 Max: 28.2 ms a frame for the
placements' own triangles, 17.2 ms instanced and culled.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 04:04:59 +00:00
bherbruck 6c60f1a9ed web-offline-test: hold responses until the test lets them go (#4620)
The slow steps waited on the clock (6 s sleeps, a 1 s timer, a 30 s cap), so on a busy
runner a launch could outlast them and time out at 240 s. The server now holds a response
until the test releases it, and the test waits on events:

- overlapping downloads: build v5's own files are held; files both builds share (index.html,
  names no build changes) are served, since the browser queues a request behind an in-flight
  one for the same URL. The held files are released once v6 has started.
- fail-fast step: the other files wait until the test has seen them asked for.
- update in another tab: the other tab updates once the launch is waiting for build.json, and
  the browser's own sw.js check waits for the same moment.
- hung requests no longer give up after 20 s.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 21:51:01 -04:00
bherbruck 33fdcdb3a0 Release 0.19.0
The tree is main at 5899e5b6c exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 21:13:33 -04:00
bherbruck 5899e5b6c5 Release 0.19.0 (#4603)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:13:21 -04:00
bherbruck e23a5bf243 Ship 14 more official themes: SolidCraft 95, Platinum 9, Cupertino, phosphor pair, Blueprint, One Dark and a pack of open-source palettes (#4602)
Registers them after the Gruvbox set in the Theme list, adds attribution rows,
and keeps the heading-band ordering check passing by nudging heading/subheading
(and, for High Contrast, dialog) backgrounds.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 21:00:11 -04: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
bherbruck 920398ac53 Bring back the view-change glide; orbit, pan and zoom stay instant (#4596)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:37:19 -04:00
bherbruck 688124937d Chamfer: a cutter that would reach into a nearby wall is not used (#4539)
* Chamfer: a cutter that would reach into a nearby wall is not used

A straight chamfer's cutter reaches several times the chamfer's size out
from its edge, and further on a larger part. On a pin standing in a socket,
that far side lands inside the socket's walls, and the cut took bites out
of them. A cutter whose far side is inside the part is now refused, as one
whose ends are, and the chamfer is built locally instead.

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

* Robustness goldens: seed 9 re-recorded for the chamfer cutter check

A chamfer in seed 9 was refused because its result changed more of the
part than a chamfer can. With its cutter refused, it now builds locally.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:05:35 -04:00
github-actions[bot] b49557c294 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-21 23:54:54 +00:00
cronos dfd58293a6 Navigation: opt-in free orbit and shared Normal to Selection (#1912)
* Navigation: opt-in free orbit and shared Normal to Selection

Preserve the complete reviewed feature, shared camera setting propagation,
rigid pivot behavior and focus/drag regressions from PR #1912.

Consolidate the prior main-integration commits before rebasing, so their
merge-resolution fixes remain in the linear feature history.

* docs: move free orbit help beside viewport controls

* Merge main into fix/solidworks-free-orbit

Camera animation is gone on main, and with it the orbit snap spring: the Shift
snap keeps its unsnapped orbit in orbit_free, which the free-orbit checks now read.

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-21 23:29:29 +00:00
raykholo ba4f7864ff Export Current View as DXF includes shown sketches (#3708) (#3780)
* claim: #3708

* Export Current View as DXF includes shown sketches (#3708)

The view export refused a design with only a sketch and left shown
sketches out when bodies were present. It now draws the sketches the
viewport shows (profile curves only: no construction geometry, points,
dimensions or constraints) on a SKETCH layer, one entity per curve.
Finished sketches are hidden behind bodies through the body-edge
occlusion; the active sketch is drawn on top, as in the viewport.
Sketch points use the same projection, budgets and cancellation.

The export is refused only when there is neither a visible body nor a
shown sketch curve; the dialog and the command share that check
(cmd::view_dxf_exportable).

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 23:09:57 +00:00
Daniel b289dc2421 Sketch Dimension: name = value defines a user parameter (#2916) (#3638)
* Sketch Dimension: `name = value` defines a user parameter (#2916)

In a dimension's value (the D tool's box, and `sketch.dimension`'s `value`
from scripts, the CLI, MCP and the control channel), `name = expression`
adds the user parameter `name` with that expression and dimensions with
`name`: typing `width = 40` on a line gives a 40 mm user parameter `width`
and a dimension whose expression is `width`. A bare number is read in the
design's unit, as a bare dimension value is; an angle's parameter is in
degrees. The expression may be anything the parameter editor takes.

It is one command: one undo removes the parameter and the dimension
together, and a dimension the sketch refuses leaves no parameter behind.
A taken name is refused with a hint (type the name to use it, or change
it in Parameters), a reserved name with the parameter editor's message,
and a left side that is not a name (`2 = 3`) still fails to parse as
before. A driven dimension defines nothing. The reply carries `defined`,
and the status line names the parameter.

The splitter lives in the doc crate (`expr::definition`) so other value
inputs can take the same form later; editing an existing dimension, the
inline shape boxes and feature dialogs are follow-ups.

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

* Sketch Dimension: say why a name is refused, in the command's own words (#2916)

The dimension editor showed a refused definition raw, in the box and the
status line: "invalid parameters for `sketch.dimension`: value: `mm` is a
unit, constant or function name". It now reads "Sketch Dimension: `mm` is
a unit name, so it cannot be a parameter name": the editor passes the
command's errors through `friendly_error` like every other command, and
drops the `value:` field tag, since the box is the value. The taken-name
hint reads the same way: "Sketch Dimension: there is already a parameter
`w`: type `w` to use it, or change it in Parameters".

A reserved name says what kind of word it is and what that means
(`param_name_problem`, shared with the Parameters dialog): a unit name, a
function name, or a constant, "so it cannot be a parameter name";
`expr::reserved_kind` tells them apart.

`friendly_error` translates the command label with the interface language
(the engine's sentence stays English), so the prefix reads "Wymiar szkicu:"
in Polish.

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

* Sketch Dimension test: keep the third line off the ribbon (#2916)

On current `main` the ribbon floats over the top of the canvas, and the
test's third line at sketch y = 50 projected under it: the click opened
Parameters instead of picking the line. The line now goes below the
first one.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-21 22:57:22 +00:00
raykholo 2daf22923c Extrude preview: a cut from an empty root is no longer refused (#3085) (#3363)
* claim: #3085

* Extrude preview: a cut from an empty root is not refused (#3085)

A cut or intersect sketched where the active component has no body of
its own (the root of an assembly, or of a design whose part was
imported into a component) takes part in every body (#1234): the
command makes the cut. The Extrude preview checked the targets itself,
looked only in the active component, and showed "there is no body to
cut or intersect" beside a tool going through the part. Pressing OK
still cut, so the dialog said one thing and the model another.

The rule now lives in one place, Session::cut_takes_every_body, which
both the command and the preview ask, so they cannot disagree again.

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 22:37:31 +00:00
bherbruck 2fdc105d87 IGES import: separate lumps of one part are separate solids (#3928) (#3972)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:26:43 +00:00
DuBearte 95b23f28d6 Rename box: Tab (and leaving the box) keeps the new name (#3313) (#3336)
* claim: #3313

* fix(ui): the Rename box keeps the new name when it loses focus (#3313)

Only Enter committed a rename: Tab (and anything else that moved focus away)
dropped the typed name. Leaving the box now keeps it, as a click elsewhere
already did; Escape still cancels.

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 22:07:38 +00:00
DuBearte 14b27f323a Dialogs picking a plane show the origin planes in the view (#2581) (#2618)
* claim: #2581

* fix(view): dialogs picking a plane show the origin planes (#2581)

Create Sketch showed the origin planes while it picked its plane; now any dialog
whose active input takes origin planes does (Mirror's plane, Offset Plane,
Midplane, the construct tools), so the plane can be picked in the view.

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 21:49:18 +00:00
takuto-NA 4ab3a85323 Bug report: clarify the form and retain margins on resize (#2244) (#2246)
* claim: #2244

* fix(ui): guide bug reports with a bounded, labelled form

* fix(ui): export bug reports after processing current-frame edits

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 21:49:11 +00: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
Mauznemo 84ec512d7a fix: midpoint tool rejects two lines (#3836) 2026-10-21 20:36:29 +00:00
github-actions[bot] 17ece2edc0 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-21 20:28:26 +00:00
Nathan Dane 63f210b49f Restyle the vertical history sidebar (#2028) (#3953)
* 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)

* 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
2026-10-21 18:56:55 +00:00
raykholo 9acff62422 Sketch trim and extend refuse an offset's ends instead of undoing silently (#3918)
An offset's output curves have their ends recomputed from the source
curves on every solve. Trimming a piece off one reported success and left
the curve whole, because the refresh pulled the new end straight back.
Extending one was refused as "connected to other geometry", which does not
say why. Both now refuse with the reason and the way out: edit the source
curves, or break the offset association first. Trimming away a whole
offset curve, which removes it from its association, still works.
2026-10-21 18:56:53 +00:00
Tyler Chiriboga 915ee8a178 Fuzzer: a chamfer may add material, as a fillet may (#3868)
The fuzzer chamfers randomly picked edges and expected every chamfer to
shrink the body, so a chamfer of a concave edge, which fills the corner,
was reported as a wrong volume: 53 of the 6,000 designs of a run (seed
7100465: a box, a shell, a chamfer of an inside edge). Fillets already
expect any volume change for the same reason; chamfers now do too.

The regression test checks both directions on a shelled 20 mm box (an
inside-edge chamfer of 1 mm adds 8 mm³, an outside one takes 10 mm³
away) and that seed 7100465 is no longer a volume finding.

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:56:51 +00:00
phanoob ac1768e08b Center circle through a snapped point stays on it (#3099) (#3277)
* claim: #3099

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

* Centre circle: a second click snapped to a point keeps the circle on it (#3099)

The tool sent the centre's snap reference but only a radius for the
second click, so a circle drawn from a line's midpoint through its end
was left free to change size. sketch.circle.center takes through: a point
reference, kept on the circle (PointOnCurve), and the tool passes it when
its second click snapped to a point.

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>
2026-10-21 18:56:43 +00:00
phanoob e1daf0a8b6 Clicking a parameter suggestion inserts it (#3257) (#3260)
* claim: #3257

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

* Clicking a parameter suggestion inserts it (#3257)

A press on a suggestion took the focus from the expression box before
the list could see the click: the list stopped drawing, the click never
landed, and the box gave up what was typed. The rows are kept from the
frame before; a press on one picks it and gives the box its focus back
(again on the release, which the box takes as a click away). The rows
are no longer focusable. The completion tests' frames now drop their
texture updates on purpose, so they run where egui checks that.

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>
2026-10-21 18:56:41 +00:00
phanoob 57c3e94df6 Parameters: a new parameter's expression starts blank, and Tab selects a field (#2919) (#3096)
* claim: #2919

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

* Parameters: a new parameter's expression starts blank, and Tab selects a box of the add row (#2919)

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

* Parameters tests: the add row's blank expression in the manage dialog tests (#2919)

The two inventory tests typed a name and pressed Enter, counting on the
10 mm the expression used to start with; they now type it.

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>
2026-10-21 18:56:39 +00:00
amolinari-autotex 5e05d925e0 Tapped Hole: keep Custom selection and edit nominal diameter (#2440) (#2441)
* claim: #2440

* feat: choose ISO metric tapped-hole sizes and derive the drill diameter

* fix: label the tap drill and hide the unused diameter editor

* test: cover tapped hole catalogue and drill sizing

* fix: omit coarse metric pitch from generated designations

* Keep custom tapped-hole nominal diameter and pitch editable

Preserve Custom as dialog state, derive the existing metric designation from numeric inputs, and refuse invalid drafts without applying the last valid size. Retain manual text editing, physical tap-drill geometry and saved-design compatibility.

* fix(i18n): register custom tapped hole labels in source catalogs

* Adapt tapped-hole Custom fields to the current form kit

* fix: preserve tapped designation focus with dynamic custom rows
2026-10-21 18:56:38 +00:00
NOWN-DEV 50313d147f solid.mirror: copies join their source's component; follow the saved placement (#1938)
* solid.mirror: copies join their source's component; follow the saved placement

Three follow-ups to mirroring bodies of placed components:

- A copy joins the component of the body it mirrors (or, when that
  component is placed more than once and a copy there would show at
  every placement, the innermost component around it placed once), so
  checks that group bodies by component see it beside its source.
- The saved placement path, not the body's name, says which component
  a mirrored body is in: inserting the same file again reuses the body
  names, and the copies of the first insert vanished.
- The mirror is evaluated again only when its own placements or its
  component's frame change, not on every occurrence move: each later
  command paid for one more mirror evaluation.

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

* solid.mirror: a saved placement must still lead to the body's component

A stale or edited placement path that leads to another component's
occurrence mirrored the body silently where that other component is
placed. The mirror now reports that the placement no longer exists.

The body's component comes from the evaluated body where a transfer
recorded it, else from the component its name is recorded in or its
feature's. When a later insert of the same file has taken over the name
(the recorded component lies outside the components the body's own
feature made), the placement has to lie inside those components instead,
so mirrors of the first insert keep working.

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

* solid.mirror: the copies' component change is a pending format change

v26 shipped in 0.15.0, so its mirror-placements note stays as released;
the change of where a Mirror with placements puts its copies is noted in
a new pending file instead.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 18:56:36 +00:00
bherbruck c07ab5f8f9 Release 0.18.4
The tree is main at 728f753b8 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 12:26:34 -04:00
bherbruck 728f753b80 Release 0.18.4 (#4362)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:30:18 -04: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
bherbruck 4b37c046e4 Sketch tools come back in the variant used last (#4308)
* Sketch tools come back in the variant used last, and it is kept in preferences

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

* Scenario: a new design keeps the remembered sketch variant

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 11:22:04 -04:00
bherbruck 54072b7014 Start page: sample pictures and the existence check of recent files run off the window thread (#4349)
A sample picked before its picture arrives still opens; the language test waits for the file check.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 11:20:12 -04:00
bherbruck cb3cabeda1 Remove camera animation: every view change and orbit applies at once (#4296)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 11:19:40 -04:00
bherbruck 775e5155bb Main green: revert the start page change (#4295) and fix the format 28 test (#4335)
* Revert "Start page: sample pictures and the existence check of recent files run off the window thread (#4295)"

This reverts commit d9059d8da5.

* Test: an older design is marked format 28 now that 0.18.3 folded face pieces into 29

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:52:08 -04:00
bherbruck e570c0b4eb Windows: a restored window that is out of reach is fitted to the monitor's work area (#4299)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:36:36 -04:00
bherbruck d9059d8da5 Start page: sample pictures and the existence check of recent files run off the window thread (#4295)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:36:27 -04:00
bherbruck 463cea0615 Windows: ask for the foreground once, when the first frame is on screen (#4284)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:17:10 -04:00
bherbruck 0225e4d2a5 Startup: the first frame no longer waits for the system CJK/Hangul/Thai font search; SOLIDCRAFT_PROFILE_STARTUP prints startup steps (#4283)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 09:17:02 -04:00
bherbruck fea32f8aac Release 0.18.3
The tree is main at 034aa60d4 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 08:46:37 -04:00
bherbruck 034aa60d47 Release 0.18.3 (#4270)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:46:14 -04:00
bherbruck 971b223246 Thread: turn a shaft thicker than a modelled thread down to its major diameter (#4256)
* Thread: turn a shaft thicker than a modelled thread down to its major diameter

A modelled external thread on a shaft thicker than the thread no longer refuses: the shaft
is turned down to the major diameter over the threaded length (run past a free end of the
face), then the thread is cut as before. The turned face is named F<n>:turned and the step
it leaves F<n>:shoulder:start/end. A shaft the part goes on around is refused rather than
cut into, shafts under the minor diameter and cosmetic threads on thicker shafts are still
refused.

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

* Thread preview test: an 8 mm shaft is turned down for M6; M10 still refused

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:27:18 -04:00
bherbruck 8c861f9f46 Face names: pieces of a split face keep their numbers across evaluations (#3902) (#3991)
* Face names: pieces of a split face keep their numbers across evaluations (#3902)

Pieces sharing a name were numbered in the kernel's order, or by the index of the operand face
they came from. A boolean can list its result's faces in another order on the next evaluation,
and an operand made by one (a closed sweep tool) carries that order into the indices, so the
#1/#2 of a split face could swap between two evaluations of the same design. Pieces that
nothing else tells apart are now numbered along the spread of their centres (as edge pieces
already were), and pieces from different faces of one operand by kind, then by place.

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

* Face names: older designs keep the piece numbers they were saved with

Numbering pieces by place renumbers split faces in many existing designs, and a stored name
matched exactly would then land on the other piece. Features of a design saved before this
change are marked `kernel_piece_order` by a pending format step and number their pieces as
before; features added afterwards number by place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:08:08 -04:00
bherbruck e04a016e7c Align takes planes and the origin (#4248)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 08:06:32 -04:00
Irakli Kourtis 93a2922c5e Display plans and the model cache for large STEP assemblies (#2814) (#4021)
* STEP import: mesh band faces that wrap around a closed surface correctly

Fixes two related bugs that caused SolidCraft to fail to mesh a face wrapping
all the way around a closed, non-periodic surface (a revolved ridge band, or a
tube-shaped B-spline surface): a missing face, a folded/self-overlapping
triangulation, or a hole in the rendered body.

1. Ring edges that run slightly off their surface. STEP files write a surface
   and the curves bounding it to the file's own declared uncertainty, so a
   ring edge can legitimately sit a few micrometres off the surface it
   bounds. The code finding where a ring edge crosses the planned split line
   compared against the edge's raw (off-surface) point using a hardcoded
   tolerance far stricter than the file's own precision, rejecting a valid
   cut. Now matches against the point on the surface directly below the ring
   crossing, respecting the file's declared tolerance.

2. Closed B-spline surfaces, their slits and their slivers. truck only treats
   the rotation of a revolved surface as periodic; a face on a B-spline
   surface whose control net closes on itself (a tube) wasn't recognized as
   wrapping around at all. This produced a folded face, a hole (a boundary
   loop landing a full period away from where it should cut), and one
   near-zero-width sliver face costing the entire body its tessellation
   plan. Adds periodicity detection for closed B-spline surfaces, period-
   aware hole-loop alignment, dropping of zero-width slit boundaries, and
   isolating a too-thin-to-triangulate face so it no longer takes the rest
   of the body down with it. The GPU shader's periodic wrap mirrors the CPU
   math so the two don't drift apart.

Regression tests: band_faces_split_when_ring_edges_run_off_their_surface,
band_face_with_a_zero_width_slit_is_still_meshed,
a_sliver_face_too_thin_to_triangulate_does_not_cost_the_body_its_plan.
Ablation-tested: disabling either mechanism in (2) reproduces its target
test's original failure message while leaving the other test unaffected.

* STEP import: mesh the band's split faces, not just split them

jyviko asked whether a band wall missing from the interactive app's render
was a shading issue or a genuinely missing face. It was missing: the ring-
edge split fix made the band's topology correct, but plan_face still
produced zero triangles for the two new sub-faces.

Two causes, both in plan_face (crates/kernel/src/tess/mod.rs):

1. The tolerance for how far an imported boundary may lie off its surface
   was tied to the requested mesh's chord tolerance instead of to the
   body's own size. A finer display tolerance could then reject a boundary
   a coarser one had just accepted, so the face would drop out exactly when
   zooming in on it.

2. Near a sharp corner of this flat-panelled band, several boundary points
   that are legitimately a few micrometres apart (within the STEP file's
   own declared precision) all projected onto the same surface point. The
   boundary loop then repeated a point in (u, v), which a triangulator
   can't follow, so the face's plan failed outright. One face without a
   plan costs the whole body its plan, and the fallback mesher can't
   handle a closed B-spline surface either, leaving both halves of the
   split band with no triangles at all: present in the topology, invisible
   in every renderer, and absent from the body's volume.

Fix: scale the off-surface tolerance to the body's own extent (new
Rules::extent), and collapse a run of boundary nodes that land on the same
surface point to one (drop_collapsed) before triangulating.

Regression test additions to band_faces_split_when_ring_edges_run_off_their_surface:
the body's volume now matches the analytic pentagon prism (was a third of
it - only the top cap - before this fix); display_plan leaves no face
without triangles from coarse to fine tolerance; and the actual tolerance
and facet angle the viewport draws with gives the split band its full
lateral area on both halves.

Confirmed on review_band_big.step, the fixture behind the review screenshots:
volume's relative error against the analytic pentagon prism drops from
1.4e-4 to 4.1e-6, and the band wall now occludes the floor grid behind it
in a snapshot the same way the top and bottom caps always did.

Full kernel suite: 739 tests, 0 failed.

* STEP import: hold an imported boundary to the file's own tolerance, not the part's size

jyviko: the off-surface acceptance added in the last commit
(rules.extent * OFF_SURFACE) scaled with the part, so a 2 m part
silently accepted a boundary 2 mm off its surface while a 20 mm copy
of the same part accepted only 0.02 mm. A STEP file's own declared
uncertainty does not grow with the part, so the bound shouldn't either.

Body gains boundary_tol: Option<f64>, Some(file's declared tolerance)
for a body read from STEP (crate::step_in::Units::tol, already used
by the ring-split fix in split.rs), None for anything this kernel
built. It's set before the import's own unmeshed-face check runs, and
carried over by the two face-split operations that re-cut an imported
surface (splitface.rs, outline.rs), since the file's uncertainty still
describes those boundaries afterward.

plan_face's accept now reads: the file's own tolerance when the body
declares one, or the old chord-tolerance/extent bound otherwise. The
old bound's tol * 10.0 term had to go too, not just the extent term:
at a 2 m part's typical display tolerance that term alone is ~20 mm,
nearly ten times the extent bound jyviko objected to, so keeping it
would have left the exact 2 m/2 mm example still silently accepted.

Two new tests in step_in/tests.rs:
- band_faces_split_when_a_scaled_copy_runs_off_its_surface: the same
  band fixture at a realistic 2 m scale with the same absolute slop
  and declared uncertainty as the small one, meshes the same way at
  every chord tolerance from coarse to fine.
- off_surface_budget_follows_the_file_not_the_part_size: the actual
  regression. The same off-surface error is accepted or refused the
  same way whether the part is 20 mm or 2 m; a 2 mm error against a
  1 µm declared uncertainty is refused at both sizes now (accepted at
  2 m under the old bound, refused at 20 mm - that asymmetry was the
  bug). Checked at both a coarse and a fine display tolerance.

Known gap, not fixed here: a STEP-imported body saved into a
.solidcraft design and reopened loses boundary_tol (native.rs's
decoder has no field to read it from) and falls back to the old
extent-based bound on reload. Today's design format has no concept of
this value at all; persisting it is a real format change, out of
scope for this fix.

Full kernel suite: 776 tests, 0 failed. clippy -D warnings clean.

* Display plans for STEP threads and pointed ends (#3847)

Three faces cost a threaded or pointed STEP part its whole display plan, so it
was drawn from the reference mesh: cracked, faceted, and never refined up close.

- A loop through a pole (a cone's apex on the boundary) seemed to wind around
  the surface: the jump at the apex, unwrapped to the nearest copy, was
  arbitrary. The loop is turned to end where it leaves the pole, so the step
  that closes it is an ordinary one (`seam::close_through_pole`).
- Projection onto a spline with hundreds of spans (a helical flank) restarted
  from a 16 x 16 grid and settled on another turn; it now starts from two
  points per span, capped.
- Sag refinement no longer inserts a point exactly on a boundary side, which
  split the side and failed the face.

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

* tess: a refused face's edges are divided finer and it is planned again (#2814)

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

* tess: sliver faces plan empty; a face no method plans is filled flat from its nodes (#2814)

A face whose boundary runs out and back along itself (two edges between the
same vertices, a rounding apart, as STEP files write slivers) encloses nothing
and is planned with no triangles. A face still refused after its edges are
divided finer (a cusp, a sliver whose sides sag across each other, a small cap
at a pole) is filled flat from its shared boundary nodes, so the rest of the
body keeps its display plan instead of falling back to the reference mesh.

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

* display mesh tests: a rim the chord tolerance leaves a line is divided again (#2814)

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

* portable B-rep: up to 8192 control points along a spline direction (#2814)

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

* solidcraft-cli: --debug logs what the engine reports to stderr

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

* STEP import: a spline written over a tiny parameter range is read over one (#2814)

Some writers parameterize splines over a few millionths (a humanoid robot's
edges span 5.4e-6). The kernel tells parameters apart only to 1e-6, so over
such a range it evaluated the curve's end wrongly, and trimming an edge to its
vertices dropped a control point: the bodies could not be stored exactly and
the whole model cache was left out of the design. The knots are now stretched
to span one; the curve is the same and edges are trimmed by their vertices, so
nothing read from the file refers to the old parameters.

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

* model cache: a body keeps its file's declared accuracy (#2814)

A body restored from the cache dropped the accuracy the file it was read from
declares, so the planner held its boundaries to the kernel's own bound and a
reopened design drew differently from the import. It is stored with the body's
runtime values, and only when there is one.

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

* tess: a boundary within a hundredth of its body lies on its surface, at every quality (#2814)

#1721 held a body read from a file to the accuracy the file declares. A writer
declares what it aims for, not a bound its edges keep: a 251-body robot
assembly declaring 0.01 µm has boundaries 10 to 30 µm off their surfaces, and
95 of its bodies lost their display plans. The bound is a hundredth of the
body again, what Normal quality always allowed, and now the same at every
quality, so zooming in still never drops a face (#1721's point). A file
declaring more raises it.

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

* STEP import: keep faces whose edge vertices sit slightly off their curves

Some exporters write an edge's vertex beside the end of its curve, further off
than the accuracy the file declares (a surface export declares
0.01 mm and puts a vertex 0.19 mm off two spline ends). The reader refused
such edges and left the face, and with single-face bodies the body, out.

A vertex may now sit up to the file's tolerance or 1/1000 of the model's size
(the diagonal around its VERTEX_POINTs), whichever is larger, never more than
the existing 1 mm ceiling. Spline and open-conic edges have the end control
point moved onto the vertex, so every such edge meets its vertices exactly.
The file's declared accuracy still governs closed-curve, seam and circle
checks.

* tess, io: review clean-ups (#2814)

- A sliver face is one whose loops are all slit: drop_slits finds it in the
  retry after a failed plan, so the pre-check on every face (folded) is gone.
- The flat fill uses cdt::triangulate (a lenient fill that leaves out sides
  crossing one already kept) and its flood-fill inside test, and the plane
  from geom::Plane.
- Refinement only builds its indexes when a face was refused, takes the
  faces to plan again from edge_faces, and re-samples edges on the pool;
  Rules::finer for the finer rules.
- One PackageWriter::fits for the model cache, the preview and the thumbnail.
- The band split's ring tolerance lives in split.rs; comments say what the
  off-surface bound is now.
- The CLI logger prints Level::as_str().

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

---------

Co-authored-by: David Lackovic <lackovicdavid1234@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Ray <raykholo@users.noreply.github.com>
2026-10-21 10:37:12 +00:00
Irakli Kourtis ff8b09c225 Kernel paused: label new kernel issues, PRs and discussions for a maintainer to answer (#3994)
While a new geometry kernel is evaluated, a workflow labels new issues, pull
requests and discussions about kernel work kernel-paused, so a maintainer finds
them and answers in person. A pull request counts when it changes
crates/kernel/ or vendor/truck*, anything else when its title names a kernel
operation. Crash and data-loss reports and anyone who can push are left alone.
CONTRIBUTING.md says so.


Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:03:47 +00:00
raykholo 975a08c992 ui-egui tests: hold OK's evaluation where a test checks it is computing (#4040)
Two OK-off-the-window-thread tests checked that OK was computing right
after Enter without holding the evaluation. The preview's evaluation is in
the cache, so the worker was usually in before OK's zero wait was over and
OK applied at once: cancel_while_computing_applies_nothing failed in most
runs (5 of 5 in a debug build, 4 of 5 with the ci profile). Both now hold
the evaluation with the test gate, as their neighbour does, and let it go
once the computing state is checked.
2026-10-21 09:38:14 +00:00
bherbruck 55587f4ac4 View keys: a held arrow turns the view at a steady rate, one tween at most (#4090)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 04:22:26 -04:00
bherbruck e94900933d IGES import: close small gaps between trim pieces (#3845) (#3846)
Consecutive pieces of a trim loop can miss each other by micrometres to a tenth of a
millimetre, past the file's resolution, so the loop did not close and the face was
dropped. Faces are now read first and sewn after: each small gap's ends, and the other
piece ends at them, move to one vertex. Gaps over 0.1 mm or not small next to their
pieces stay open; the sewing tolerance stays the file's. The stated accuracy covers how
far a vertex moved, so the STEP reader keeps those edges.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:20:02 +00:00
SS-EN 776541b081 Agent commands compute off the window thread, so the window stays live (opt-in) (#1989) (#3340)
An agent's design changes over the control channel (engine.execute, engine.batch) are evaluated
ahead on a copy of the session on an `agent` worker, then run on the window thread reusing every
feature computed there (doc::KeptResults, whatever the bounded evaluation cache dropped); the
worker also makes the display meshes of the bodies they end with. Exports run on a copy. While a
change computes, or the agent cursor acts one out, ui.*, inspection and screenshots answer; later
design changes wait their turn. A chip names the command with Stop. A person's edit meanwhile goes
first; a replaced design refuses the request. Preference: Preferences > AI, agent_off_thread, off
by default.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:58:56 +00:00
raykholo 79a843c963 Transform: refuse a matrix that is not an affine placement (#3900)
transform_matrix took any finite 4x4 matrix. A perspective entry in the
last row, or a singular linear part, was built into faces that mesh to
nonsense: a revolved ring placed with such a matrix measured hundreds of
millions of mm3, and a boolean on it ran for over half an hour before
failing with "no result". Matrices come from design files and command
parameters, so they are refused with KernelError::Invalid instead.
2026-10-21 00:58:49 +00:00
AlF 1bcc3602b4 STEP export: a merged cylinder or cone face's loop starts on the side its angle grows from (#3394) (#3557)
* claim: #3394

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* STEP export: a merged cylinder or cone face's loop starts on the side its angle grows from (#3394)

A whole-turn cylinder or cone face is written as one loop: ring, seam, ring, seam reversed. Readers lay the ring that follows the seam's first use out from the surface's angle 0, so that ring must turn the way the angle grows (anticlockwise about the written axis). Three of the four tapered legs in #3394 came out the other way and other readers dropped their sides. When the second ring turns against the angle, the loop now starts with it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-21 00:58:47 +00:00
raykholo 93684a4728 Expressions: a leading decimal comma such as ,5 is accepted (#2966) (#3797)
* claim: #2966

* Expressions: a leading decimal comma such as ,5 is accepted (#2966)

In a comma-decimal locale `,5` means 0,5, but the lexer only started a
number on a digit or a point. A comma now starts a number outside
parentheses when a digit follows and an operand is expected (at the
start or after + - * / ^). Commas after an operand keep their meaning,
so function arguments and the existing error cases are unchanged.
2026-10-21 00:58:44 +00:00
raykholo 9db39f3d7d Holes from a sketch on a body's face drill that body (#2882) (#3770)
* claim: #2882

* Holes from a sketch on a body's face drill that body (#2882)

A multi-point hole from a sketch attached to a body's face chose its
side and moved its points onto whichever body lay along the direction,
then cut every body, so points on a front body's face could drill the
body behind it. `targets` was accepted and ignored.

- solid.hole: holes at the points of a sketch on a body's face store
  that body as their participant (the lookup a face-sketch Join uses);
  `targets` sets the bodies to drill, `["*"]` every body; explicit
  participants still override.
- Evaluation: for sketch-point holes with participants, the drilling
  side, the surface each point moves onto and pattern copies are found
  on those bodies only.

Sketches on planes, position and Single holes, and saved holes without
participants recompute as before; no design-format change.
2026-10-21 00:58:43 +00:00
Colin Faulkingham 29369a0bb5 Extrude to an edge or corner ends at the requested plane (#3729) (#3748)
* claim: #3729

* Fix extrusion targets at planar face boundaries (#3729)
2026-10-21 00:58:41 +00:00
kezmdor 5a9e771cc9 io: the 3MF and DXF budget tests do not check wall-clock time (#2998) (#2999)
* claim: #2998

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

* io: the 3MF and DXF budget tests do not check wall-clock time

nested_fan_out_of_empty_objects_is_bounded spends about 20 million
visits reaching the work budget: 1.5 s alone, up to 40 s and more when
every test binary runs at once, past its 30 s limit. The time checks
run only after the reader returns, so a missing budget hangs these tests
with or without them; the error, the empty geometry and the three
vertices per placement are what show the budgets work. CONTRIBUTING.md:
a test must not depend on wall-clock timing.

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

---------

Co-authored-by: kezmdor <15539254+kezmdor@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:58:39 +00:00
NOWN-DEV 63cb48a4ee Measure: a body whose meshes leave a gap keeps its volume wherever it is (#2069)
* Measure: a body whose meshes leave a gap keeps its volume wherever it is

A part ring cut by solid.combine (two prisms through its ends, a notch
through its chamfer) measured 992.03 mm3 at the origin, 999.17 moved
100 mm and 1063.48 moved 1000 mm; it encloses 993.92. The reference
mesher leaves a crack where the notch meets the chamfer's cone: the
volume beside it is left out, and the divergence-theorem sums, taken
about the world origin, close the crack by a cone to the origin, so the
error grows with the body's distance from it.

A closed surface's area vectors sum to zero. When the picked meshes'
sum could move the volume by more than 1e-5 within the body's size, and
the display plan (which shares every edge's nodes between its faces) is
closed, the body is measured on the plan, about its box centre. Every
other body measures exactly as before.

Tests: the cut ring at the origin and moved by up to 10000 mm, against a
grid quadrature of the cut; a revolved ring of cones and cylinders moved
100/1000/10000 mm along each axis (1e-6); a cylinder and a frustum far
from the origin against hand values. Robustness seed 64 moves by 1.4e-6
and is recorded.

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

* Measure: a gap the display plan can't close is measured about the box's centre

When the meshes leave a gap and the display plan isn't closed either, the
sums used to fall back to the origin-based volume, which drifts with the
body's distance from the origin (7 % at 1000 mm for the part ring). The
per-face sums are now taken about the fine mesh's box centre, which closes
the gap by a cone to a point inside the body; over a closed mesh nothing
changes. The gap decision moves into gap_closed, and a unit test with a
cube left open by hand covers closed vs open meshes, the closed plan, and
the fallback with an open plan or none.

Seeds 14, 20, 40, 55 and 61 end with such a body and are recorded with
ROBUSTNESS_GOLDENS_BLESS (volumes move by -0.51 %, +0.16 %, +1.9e-4,
+9.0e-5 and -1.5e-5; steps, outcomes and counts unchanged).

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

* Record robustness seed 86 after measuring bodies whose meshes leave a gap

Seed 86 was added on main by #2387 after this branch's goldens were recorded. Its last
body leaves a gap in its meshes, so the measure change here moves its volume by -0.27 %
and its area by +0.13 %; steps, outcome and counts are unchanged. Seed 86 passes
unchanged on main 57d21013. Every other seed matches on the merged branch.

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

* measure: take the display plan only where it agrees with the sums to within what the gap could move

A closed display plan meshed less finely than the sums differs from them by its own chord error,
which can be larger than anything the gap explains: an imported part with small blends measured
0.03 % low on its plan. The plan is now taken only where the two differ by at most what the
gap's area could move the volume within the body's size; otherwise the sums about the box's
centre stand.

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

* measure: sum a mesh left open about its box's centre only, without the display plan

The closed display plan meshes some faces of an imported B-spline part
more coarsely than the sums do, so a body and its boolean pieces no
longer add up: clearance's union cross-check then refused a 17.3 mm3
overlap between two such parts (the pieces implied 20.5). Summing about
the box's centre alone keeps the gap closed by a cone to a point inside
the body and stays consistent with the pieces. Seeds 64 and 86 are
re-recorded with the reason.

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

* measure: placement tests in the area's test files

The ring and frustum tests join measure.rs's tests, and the cut ring
joins measure_centre_tests.rs (AGENTS.md: a regression test goes into
the existing test file for its area). The goldens note for seeds 14, 20,
40, 55 and 61 describes the centre-only sums.

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

* measure: keep the origin for a body left open on purpose

A surface, or a cup whose lid is still a separate sheet, has no volume of
its own. Measured about its box's centre, two such bodies stopped
comparing once the centre moved (a crowned lid rises), which broke the
patch crown test. Only a mesh with a small crack is measured about the
centre now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

* Robustness goldens: keep main's notes, one note for this change's seeds

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 00:57:03 +00:00
Colin Faulkingham 36c732ddb7 Shell: show the tangent-chain face selection and explain full-body removal (#209) (#3809)
* claim: #209

* Shell: show tangent-chain faces and explain full-body removal (#209)
2026-10-21 00:54:34 +00:00
Mateusz Sych fdec948abc Control channel: control.running lists what the app is running, across connections (#3654) (#3689)
* claim: #3654

* Control channel: control.running lists what the app is running, across connections (#3654)

control.pending answers only for the connection that asks, so a second client,
the MCP bridge after a reconnect or a script saw {"pending": null} while the app
was busy. control.running answers for the whole app, the same on every
connection and at once (the connection's own thread answers it, also while it
is busy with a timed-out request): every request the app has been sent and not
answered yet, oldest first, with its connection, id, method, command id (or a
batch's step count), seconds since it arrived and whether it still waits its
turn. A timed-out request stays listed until the app answers it, also after its
connection closed. Parameters are never shown.

The window marks a request started when it takes it (ControlRequest.started),
which tells queued from running. The MCP server gets a read-only `running` tool.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-21 00:54:33 +00:00
bherbruck 7803b3968f Fix main: a test built Applying without the new seen field (#4001)
#3609 added Applying::seen; dialogs_sketch_tests' apply_keeps_the_panel_open_after_asynchronous_evaluation
still built the struct without it, so ui-egui's tests stopped compiling on main. The test stands
for a job that is already being shown, so seen is true (the behaviour it had before).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:41:24 -04:00
JEREMY cbac4eab3f OK off the window thread: a job that has just started waits a frame (#2929) (#3609)
* claim: #2929

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

* OK off the window thread: a job that has just started waits a frame (#2929)

The off-thread evaluation of a feature dialog was taken up in the same frame
it started, so a job whose worker had already finished was applied at once
and the dialog never said it is computing. Under load the window thread is
held up between starting the job and the check, and the feature appears
where the computing state belongs - which is what made
`apply_off_thread_tests::cancel_while_computing_applies_nothing` fail with
`assertion failed: h.app.applying.is_some()`.

A job that has just started now waits one frame: its result is taken up from
the next one, so the dialog says it is computing before the feature appears.

The app waits APPLY_AT_ONCE (150 ms) for the evaluation before it lets the
window go on, so a job only exists when the evaluation took longer than
that: the change shows the computing state for a frame in the 150-151 ms
window, and nothing else moves.

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

---------

Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-20 23:02:34 -04:00
bherbruck 08eff4f9dd Sketch on a face: project from the model's own body, not a copy that meshes it again (#3981)
A sketch attached to a face is evaluated in the face's frame. When that frame is the body's
own, the body was still rebuilt as a new ModelBody with empty mesh cells, so projecting the
face's edges display-meshed a throwaway copy and the next cut or join meshed the same body a
second time. On a body whose cut edges are slow to mesh this added about a second to each
feature after a sketch on a face. In place, the anchored body is now the model's own clone and
shares its mesh.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:02:22 -04:00
raykholo 9b80163421 Extrudes from a nested STEP part's face join and cut it with the imported root active (#3223, #2872) (#3852)
* claim: #3223

* Extrudes from a nested STEP part's face join and cut it with the imported root active (#3223, #2872)

Opening a STEP assembly leaves its root component active, and the root
owns no body: its parts are components inside it. A profile sketched on
such a part's face was therefore treated as outside the active scope.
Join made a separate body instead of joining the part, and the automatic
operation saw no material and offered New Body in either direction; an
automatic extrude that came out as a cut found nothing to cut.

A component with no body of its own now works on the bodies of the
components inside it whose geometry lies in its own frame (as an import
places them): Join targets the body whose face the sketch is on, the
automatic operation looks at those bodies, and an extrude whose own
operation is Cut or Intersect takes part in every body as an explicit
cut already did.
2026-10-21 00:54:16 +00:00
Tyler Chiriboga 164554edba 3MF import: production extension review follow-ups (#2089) (#3628)
* claim: #2089 (3MF production extension: review follow-ups)

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

* 3MF import: production extension review follow-ups (#2089)

Follow-ups from the review of #2127:
- Only the root model's build is read; other parts' build sections are
  ignored as the spec asks (a slicer's child build pointing at a thumbnail
  no longer matters).
- A model part that can't be read fails only the placements that need it;
  the package-wide caps (part names, 1 GB of model XML) still refuse the
  package. The root XML is dropped once parsed and each part is read
  against the remaining budget.
- Loops are reported apart from depth, keyed by (part, object).
- Messages name parts as written; objects are kept in id order so parts are
  read in the same order every run; %XX escapes in part names are decoded;
  the model unit is a local of the parser.
- Tests: a component without p:path stays in its part (T1), the same id in
  two parts is no loop (T2), a slicer-shaped project (T3), the placement
  budget across parts (T4), a loop through three parts and a chain past the
  depth cap (T5), broken parts and child builds (T6), escaped names; the
  hostile-parts test asserts its messages; the mutation test also mutates
  the parts' XML and zips it again.
- Guide: what slicer-only data is not read.

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-21 00:54:14 +00:00
Matthew Knutson 453c6bcb14 Split schema contracts per command file and add shared field types (#2829) (#3558)
* Split the schema contracts into one module per command file

`schema/commands.rs` becomes `schema/features.rs` (solid.box, solid.extrude,
FeatureResult) and `schema/inspect.rs` (engine.describe), each with its own
tests file holding its `schemas_and_the_key_check_agree` key lists and its
`strict_keeps_todays_errors` cases, so conversions of different command files
touch different files. The shared loops become helpers in `schema/tests.rs`;
the assertions are unchanged. `engine.describe` gains a key-list test, and
`schema_defaults_are_valid` now covers every command with a contract.

No behaviour change.

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

* Add shared schema field types for the next conversions

For the feature commands to be converted next, each deciding acceptance with
the reader the handlers already use:

- `FaceRef` and `FaceRefs` (`face::face_ref`, `features::face_points`)
- `EdgeRef` and `EdgeRefs` (`features::edge_points`)
- `SketchRef` (`features::sketch_id`)
- `FeatureCommon`: `name`, `body_name`, `body_names` and `participants`,
  which `add_feature` reads for every feature, flattened into a contract
  with `deserialize_with = "feature_common"`, which keeps the key in the
  message and the issue's path.

The shape checks of `face_ref`, `edge_points` and `sketch_id` (and
`feature_sketch`) move into pure helpers (`face_ref_form`, `edge_ref_form`,
`sketch_key`) that the handlers and the types share; the handlers behave as
before. Tests check that each type and its schema accept what the reader
accepts (apart from a face name's byte cap and an index written `1.0`),
that the contract's messages match the handlers' errors, and that the
handlers still look picks up one at a time.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:54:13 +00:00
phanoob f44ff18ca4 Split Body: each piece can be renamed on its own (#3510) (#3527)
* claim: #3510

* Split Body: each piece can be renamed on its own (#3510)

A split named its new pieces after the split body every time (`Top (1)`),
so renaming a piece did nothing, and renaming the split body renamed the
other piece too (and broke the features that used it). The pieces now
take the names body.rename stores, and a rename keeps the names later
features derive from the renamed body.

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>
2026-10-21 00:54:11 +00:00
raykholo 98cd67f029 Joins, cuts and intersects meet their tools where they are shown, across placed components (#3383) (#3384)
* claim: #3383

* Combine: tools from another component meet the target where they are shown (#3383)

Bodies are evaluated in their own component's frame and an occurrence's
placement is applied only for display. Combine passed tool bodies to the
boolean untouched, so a tool in a component placed differently from the
target's was read as if its coordinates were in the target's frame: a
join that overlapped on screen gave two separate shells, and cuts and
intersects missed the same way. Any placement difference did it, a turn
or a plain move, in either direction.

Combine now moves each tool by the inverse of the target's placement
times the tool's own, the same placement used to show the body, keeping
face lineage. A tool placed exactly like the target is left untouched,
so combines within one component give the same result as before.
Moving an occurrence now re-evaluates a combine that depends on it.

* Joins, cuts and intersects draw their tools where they are shown (#3383)

A primitive, extrude, revolve, loft, sweep, pipe or coil that joins,
cuts or intersects bodies draws its tool in its own component's frame,
but each body is kept in its own component's. A target in a component
placed differently (an occurrence moved or turned, or a body moved into
it) met the tool as if the tool's coordinates were in the target's
frame: a join that overlapped on screen gave a separate lump 100 mm
away, and cuts missed the same way.

While such a feature evaluates, the tool now goes into each body's frame
(the inverse of the body's placement times the feature's component's)
wherever the join, cut or intersect meets it, and a body joined into one
placed differently is moved into its frame. Bodies placed as the feature
is are left untouched, so results within one component are unchanged.
Features that name the bodies they change re-evaluate when an occurrence
moves.

This also settles the moved-fastener cut of #439 (Combine, earlier commit).

* Through all and an extrude's contact tests see targets where they are shown (#3383)

An extrude or revolve measured its through-all length, and tested where
its tools meet the targets (a profile on a target's face, coplanar
growth, holes over empty space, taper run-on), on the targets as stored
in their own component's frame. With the tool now moved into a target
placed differently, a through-all cut drawn from the root under a cube
in a component moved 100 mm up was cut only as far as the cube is tall
in its own frame, and missed it.

These features now see the bodies they change that are placed
differently moved into their own component's frame, the same moves the
tools are taken back through, so the length reaches the target where it
is shown. Bodies placed as the feature is are read as before.
2026-10-21 00:54:09 +00:00
raykholo d9d6af51c6 Sheet metal: mirrored and copied sheet bodies stay sheet metal (#3353) (#3354)
* claim: #3353

* Sheet metal: mirrored and copied sheet bodies stay sheet metal

Mirroring a sheet metal body into a new body gave a plain solid: the
opposite-hand bracket had no flat pattern, DXF or bend table
("`Body1 (2)` is not a sheet metal body"). Copies from a body pattern or
Move with copy lost their sheet model the same way.

A copy now takes the sheet model with it (SheetBody::copied). A rigid
copy moves the flat frame. A mirror moves the frame with its x axis
turned round, so the thickness side stays, and mirrors everything in
flat coordinates (x -> -x): base, flange bend lines (run from their far
end, with their end extensions, slopes and tears swapped), panel shapes,
cut-outs, pockets, bores, letters and base sketch lines. Arcs stay exact
and loops keep their sense. A perforation field of a mirrored copy runs
its grid from the right, so its holes are the mirror image too. The flat
length, bends and K-factor are unchanged, and the copy takes further
sheet metal features of its own.

Mirror with combine is unchanged: the joined body has no single sheet
model yet. Mirroring or patterning flange features is still refused.
Sheet models are derived from the timeline, so no design format change.

* Format: note the saved from_right of a mirrored sheet's perforation field

The new `from_right` flag is saved in the design, so it gets a pending
format note. No migration: a field without it runs from the left as
before.
2026-10-21 00:54:07 +00:00
Christian Svensson 38c710e72e Save designs with large STEP imports: compress the STEP part (#3221) (#3285)
* claim: #3221

* Save designs with large STEP imports: compress the STEP part (#3221)

A STEP between 256 and 512 MiB imported fine but could not be saved: a
design stored the STEP text uncompressed, and a design part is capped at
256 MiB. STEP sources are now always Deflated (level 2), may be as large
as an import accepts inside the file, and an import that could not be
saved even compressed is refused with a clear message. Reading stops at a
part's declared size, so a decompression bomb is refused early.
2026-10-21 00:54:05 +00:00
raykholo 5b5b051e02 Delete in Edit Joint deletes that joint, and a refused joint delete says why (#3749) (#3776)
* claim: #3749

* Edit Joint: deleting the joint being edited closes the dialog and deletes it (#3749)

A joint delete was dropped without a word while any dialog was open, and
Edit Joint is a dialog. Deleting only the joint Edit Joint edits now
cancels the dialog first (as its Cancel does) and goes through the usual
selection.delete dry run; Delete with nothing else selected in Edit Joint
targets that joint. Every other refusal (another dialog, a tool, a
computing history, a loading file) now says why in the status bar.
2026-10-21 00:49:50 +00:00
kaltenbrunnermarkus 686576e368 Section Analysis: Edit reopens the dialog where the section was left (#3617)
Edit Section Analysis (its menu item, or a double-click on its browser row)
opened the dialog at distance 0 with no plane, so moving the cut on by a
millimetre meant picking the plane and typing the whole distance again.

OK now keeps the dialog's plane pick, distance and flip with the cut they
made, and Edit starts from them while the section shown (or hidden) is still
that cut. The toolbar still starts a new section afresh, and a section made
some other way (a command, an agent) opens the dialog as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:49:48 +00:00
rlartot 68e4186080 fix(ai-chat): replay with reasoning_effort none when OpenAI refuses tools (#3798) (#3817)
* claim: #3798

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

* fix(ai-chat): replay once with reasoning_effort none when tools are refused (#3798)

Newer OpenAI models answer HTTP 400 to function tools on /v1/chat/completions
unless reasoning_effort is "none". On that exact refusal the request is sent
again once with it, and later requests of the chat carry it directly.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 00:44:52 +00:00
Colin Faulkingham b66ad0a9c9 Move shortcut respects the selected component (#3580) (#3588)
* claim: #3580

* fix(ui): move the selected component from its shortcut (#3580)
2026-10-21 00:44:48 +00:00
amolinari-autotex 2eacbc37f4 Keep sketches out of the Browser Features folder (#3584) 2026-10-21 00:44:43 +00:00
phanoob f2f9c6fe27 Settings › AI: Add agent opens from the Home tab too (#3483, #3513) (#3522)
* claim: #3483

* Settings > AI: Add agent opens from the Home tab too (#3483, #3513)

The Add agent dialog was drawn by the chat panel, which Home doesn't
show, so Add agent did nothing while Home was open. It is now drawn
from the frame itself, before Home and the workspace part ways.

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>
2026-10-21 00:44:41 +00:00
SS-EN d21e2b1afd Keep picked points when a type-first dimension turns horizontal or vertical (#3168)
Type-first (pick, then type), the dimension's text follows the pointer, and where it goes
picks the kind: across from two points or a slanted line the distance between them, above or
below the horizontal span, beside them the vertical one. The draft kept the value of the kind it
started as, so moving from diagonal to horizontal previewed (and Enter applied) the aligned
length as the horizontal span: the solver moved the picked points (the first dimension of a
sketch even scaled it), so the dimension's extension lines no longer met them.

While nothing has been typed, moving the text now re-derives the draft for the new place, so
its value is what that kind measures and stays selected for typing. A typed value is kept.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:44:40 +00:00
1506 changed files with 406944 additions and 12171 deletions
+57 -1
View File
@@ -42,7 +42,6 @@ filter = "binary(action_matrix) & test(=action_matrix_sample)"
threads-required = 12
priority = 100
# The slowest tests start first, so a shard does not end with one of them running alone on a
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
@@ -69,3 +68,60 @@ filter = """
))
"""
priority = 100
# Each nextest test has its own process/device. Concurrent cold lavapipe path
# compiles contend for CPU and can exhaust the 600 s pipeline setup budget.
# Serialize this module per CI shard; other tests retain their normal parallelism.
[test-groups.gpu-path]
max-threads = 1
[[profile.ci.overrides]]
filter = "package(=solidcraft-gpu) & test(/^path::/)"
test-group = "gpu-path"
# Full local validation opts into its own scheduling policy. Keep GitHub CI's
# worker reservation and slow-test priorities above independent of this runner.
[profile.local-ci]
inherits = "ci"
[[profile.local-ci.overrides]]
filter = "binary(action_matrix) & test(=action_matrix_sample)"
threads-required = "num-cpus"
priority = 100
[[profile.local-ci.overrides]]
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
priority = 99
[[profile.local-ci.overrides]]
filter = """
(package(solidcraft-engine) & (
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
| test(/^tests::robustness::/)
| test(=tests::hostile_params_never_panic)
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
| test(=tests::pleat_join::the_pleats_join_at_every_wall_from_2_4_to_2_6_mm)
| test(=tests::flush_join_loft::a_flange_flush_with_the_end_of_a_bent_tapered_tube_joins)
| test(=cmd::sheet::tests::perforated_sheet_takes_a_thousand_round_holes)
| test(=boolean_regression_tests::joined_loft_frames_plan_their_display_at_every_quality)
))
| (package(solidcraft-engine) & (binary(atomic_commands) | binary(step_export_regressions)))
| (package(solidcraft-kernel) & (
test(=curveblend::tests::rolling_ball_turns_about_a_rounded_corner_of_its_radius)
| test(=tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole)
| test(=tspline::tests::hostile_tsplines_never_panic)
| test(=fill::tests::regular_rims_of_three_to_eight_sides_fill_with_one_face_and_close_the_cup)
| test(/^issue_788_tests::equal_bores_crossing_/)
))
"""
priority = 98
# Native Windows shader/device creation must not overlap within the local run.
[test-groups.windows-gpu]
max-threads = 1
[[profile.local-ci.overrides]]
platform = "cfg(windows)"
filter = "package(solidcraft-gpu) | package(solidcraft) | (package(solidcraft-ui-egui) & test(/(offscreen|gpu)/))"
test-group = "windows-gpu"
+3
View File
@@ -0,0 +1,3 @@
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
.gitattributes text eol=lf
/assets/icons/ui/design/requested_icons.txt text eol=lf
+6
View File
@@ -2,6 +2,12 @@
Describe the problem and resulting behavior. Link the issue when applicable.
Changes a default: no
<!-- Required line, replace "no" if this changes what a user gets out of the box (a default value, key binding, setting, tool behaviour):
Changes a default: yes — <what>, old behaviour kept behind preference <name>
Docs-only and bot PRs are exempt. -->
## Validation
What did you run, and what were the results? Include expected and measured values for geometry changes.
+6 -2
View File
@@ -280,6 +280,10 @@ jobs:
run: cargo xtask licences --check
- name: Dependency layers
run: cargo xtask layers
- name: Universal UI containers
run: |
cargo xtask ui-containers
cargo test -p xtask ui_containers --locked
- name: Sorted lists
run: cargo xtask lists
- name: Design format pending changes
@@ -290,8 +294,8 @@ jobs:
run: python3 -I scripts/ci/test_affected.py
- name: Merge-of-main detection self-test
run: python3 -I scripts/ci/test_main_merge.py
- name: Claim and triage bot tests
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js
- name: Claim, triage and kernel-paused bot tests
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js scripts/pr-checks/*.test.js
# Fast feedback for a pull request that has not been approved for the full CI yet.
quick-compile:
+9 -3
View File
@@ -1,6 +1,8 @@
# Files generated from the code are rewritten here, on main, after each merge, so pull requests
# never edit them (two pull requests that each regenerated one would always conflict):
# THIRD-PARTY-LICENSES.txt (`cargo xtask licences`) and docs/features.md (`cargo xtask features`).
# Pending files (assets/locales/pending, attribution/pending) are folded into the catalogs and
# ATTRIBUTION.md by `cargo xtask fold-pending`.
name: Generated files
on:
@@ -17,6 +19,8 @@ on:
- "crates/engine/**"
- "apps/solidcraft-cli/**"
- "xtask/**"
- "assets/locales/pending/*.json"
- "attribution/pending/*.md"
- ".github/workflows/generated.yml"
workflow_dispatch:
@@ -48,13 +52,15 @@ jobs:
run: cargo xtask licences
- name: docs/features.md
run: cargo xtask features
- name: Pending locale strings and attribution rows
run: cargo xtask fold-pending
- name: Commit if changed
run: |
if git diff --quiet -- THIRD-PARTY-LICENSES.txt docs/features.md; then echo "unchanged"; exit 0; fi
if [ -z "$(git status --porcelain)" ]; then echo "unchanged"; exit 0; fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add THIRD-PARTY-LICENSES.txt docs/features.md
git commit -m "Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]"
git add -A THIRD-PARTY-LICENSES.txt docs/features.md assets/locales ATTRIBUTION.md attribution
git commit -m "Regenerate generated files and fold pending files [skip ci]"
for _ in 1 2 3; do
git pull --rebase origin main && git push origin HEAD:main && exit 0
sleep 10
+42
View File
@@ -0,0 +1,42 @@
# While a new geometry kernel is evaluated, new issues, pull requests and discussions about kernel
# work are labelled `kernel-paused` for a maintainer to answer (CONTRIBUTING.md, "Kernel work is
# paused"). The logic is in scripts/kernel-paused/ (tested in ci.yml).
name: Kernel paused
on:
issues:
types: [opened]
# pull_request_target so PRs from forks can be labelled. It runs the base branch's
# scripts/kernel-paused and only lists the PR's changed paths; the PR's code is never checked out.
pull_request_target:
types: [opened]
discussion:
types: [created]
permissions:
contents: read # to check out scripts/kernel-paused
issues: write
pull-requests: read # to list a PR's changed paths
discussions: write
concurrency:
group: kernel-paused-${{ github.event.issue.number || github.event.pull_request.number || github.event.discussion.number }}
cancel-in-progress: false
jobs:
paused:
name: Kernel paused bot
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/kernel-paused
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const paused = require('./scripts/kernel-paused/kernel-paused.js');
const done = context.eventName === 'discussion'
? await paused.handleDiscussion({ github, context })
: await paused.handleOpened({ github, context });
core.info(done);
+2
View File
@@ -39,6 +39,8 @@ jobs:
run: python3 packaging/tests/nightly_test.py
- name: Windows executable icon validation regression
run: python3 packaging/tests/windows_icon_test.py
- name: AppImage CLI wrapper regression
run: python3 packaging/tests/apprun_test.py
- name: Flatpak desktop smoke validation regression
run: python3 packaging/tests/flatpak_test.py
- name: PowerShell
+69
View File
@@ -0,0 +1,69 @@
# Two quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
# for an issue a lower-numbered one already closes, and the "Changes a default:" line is filled in.
# Neither is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
name: PR checks
on:
# pull_request, never pull_request_target: nothing here checks out or runs the PR's code, and the
# token is read-only for fork PRs. Only scripts/pr-checks from the PR's own merge commit is run.
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
concurrency:
group: pr-checks-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
duplicate:
name: Duplicate PR check
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
const r = await checks.checkDuplicate({ github, context, core });
core.info(JSON.stringify(r));
defaults:
name: Defaults line check
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
const r = await checks.checkDefaults({ github, context, core });
core.info(JSON.stringify(r));
# Labelling needs write access, which a fork PR's token never has, so it is its own job and best
# effort: a fork PR just doesn't get the `changes-default` label from here.
label:
name: Label changes-default
runs-on: ubuntu-24.04
timeout-minutes: 5
continue-on-error: true
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
await checks.syncDefaultLabel({ github, context, core });
+65 -8
View File
@@ -4,11 +4,12 @@
# comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes
# the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases.
# At midnight Eastern, changed main commits build and publish a nightly pre-release.
# A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet.
# A version with a suffix (0.2.0-rc.1) is a pre-release.
# The macOS app is signed and notarized when the APPLE_* secrets exist, nothing else is signed yet.
#
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
# macOS universal .dmg, and the CLI as a .zip packaging/macos
# macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) packaging/macos
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
# Web a .zip of the static site packaging/web
#
@@ -203,6 +204,8 @@ jobs:
name: Snap ${{ matrix.arch }}
needs: [version, linux]
if: needs.version.outputs.build == 'true'
# The Snap Store hasn't accepted SolidCraft yet, so a snap failure never holds up a release.
continue-on-error: true
# Repackages the Linux job's tarball, like the Flatpak. On 24.04 because the core24 base is
# Ubuntu 24.04, which lets snapcraft build on the runner itself (--destructive-mode).
runs-on: ${{ matrix.runner }}
@@ -293,11 +296,19 @@ jobs:
path: dist/release/*
if-no-files-found: error
macos:
name: macOS universal
# The two halves of the universal binary build in parallel on their own runners (each alone
# took about half of the old single job's 45 minutes); `macos` below joins and packages them.
macos-build:
name: macOS build ${{ matrix.arch }}
needs: version
if: needs.version.outputs.build == 'true'
runs-on: macos-14
strategy:
fail-fast: false
matrix:
include:
- { arch: aarch64, target: aarch64-apple-darwin }
- { arch: x86_64, target: x86_64-apple-darwin }
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
@@ -305,12 +316,58 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-macos-universal
- name: Build and package (dmg, CLI zip; the dmg is mounted and checked)
run: packaging/macos/package.sh --arch universal
key: release-macos-${{ matrix.arch }}
- name: Build
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
# Not named release-*: the release job collects every release-* artifact as an asset.
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: macos-bin-${{ matrix.arch }}
path: |
target/${{ matrix.target }}/release/solidcraft
target/${{ matrix.target }}/release/solidcraft-cli
target/${{ matrix.target }}/release/solvecraft-cli
if-no-files-found: error
macos:
name: macOS universal
needs: [version, macos-build]
if: needs.version.outputs.build == 'true'
runs-on: macos-14
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: macos-bin-aarch64
path: target/aarch64-apple-darwin/release
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: macos-bin-x86_64
path: target/x86_64-apple-darwin/release
- name: Set up Developer ID signing and notarization
# Optional until the APPLE_* secrets exist (docs/releasing.md, "macOS signing"). Without
# them (forks, dry runs) the app keeps its ad-hoc signature and is not notarized.
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: packaging/macos/signing.sh --setup
- name: Join, sign, package and check (dmg, CLI zip; the dmg is mounted and checked)
run: |
# Artifacts don't keep the executable bit.
chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli
packaging/macos/package.sh --arch universal --skip-build ${MACOS_SIGN_IDENTITY:+--sign}
- name: Remove the signing keychain and API key
if: always()
run: packaging/macos/signing.sh --cleanup
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-macos
+1
View File
@@ -7,6 +7,7 @@
/.claude/worktrees/
/target-snapshot
__pycache__/
.vscode/
# cargo-mutants output (docs/mutation-testing.md)
mutants.out*/
+18 -2
View File
@@ -33,9 +33,12 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
and calls through its function pointers), two functions in
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
records crashes inside native code such as graphics drivers) and the Windows-only
records crashes inside native code such as graphics drivers), the Windows-only
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
test of the window without OS decorations).
test of the window without OS decorations, and gives its class the icon the taskbar falls back
on) and the Windows-only
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
mice, so no other device is sent anything).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
returns an error ("not supported yet: …"); it never panics.
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
@@ -88,6 +91,14 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **One outer UI container**: all panels, floating sheets, menus, overlays and platform UI
windows use `ui_kit::Container` (directly or through `Panel`, `Window`, `Confirm`, `Menu`).
Declare the semantic `Surface`; workspace panels use its shared layout policy and chrome.
System/report windows use `ui_kit::Window`; its low-level host builder is kit-private.
Do not construct raw egui hosts, outer popup/window frames or shadows in feature modules.
`cargo xtask ui-containers` recursively checks source/examples in all current and future crates
and apps. It runs in the fast repository CI job and `cargo xtask ci`, with no caller allowances.
See `docs/ui-container-audit.md` for the inventory and legitimate content/native exceptions.
- **One set of camera controls**: every page or canvas with a camera (viewport, Render
preview, future flat pattern or drawing pages) calls `navigation::handle` with its `NavCaps`
(orbit off for flat 2D pages). New canvases must use `navigation::handle`; never hand-roll
@@ -145,6 +156,9 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
- If someone already has it, comment on their PR or issue instead of opening a second one. Two
PRs for one change can't both land, and one of them is wasted work and CI.
- Keep the `Changes a default:` line in the PR description filled in, and don't open a second PR for
an issue a lower-numbered open PR already closes; `pr-checks.yml` fails both (CONTRIBUTING.md, "Sending a change").
## Claim = a draft PR
- Before working on issue #N, check nobody has it: `gh pr list -R tangency-labs/solidcraft --state open --search "#N"`.
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
@@ -204,3 +218,5 @@ workflow; never regenerate them in a PR.
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
then by id, generated files left to `main`, and a design-format change as one new file.
New UI strings go in a new `assets/locales/pending/<slug>.json` and new ATTRIBUTION.md rows in a new
`attribution/pending/<slug>.md`, never into the catalogs or the table (see the README in each folder).
+507 -3
View File
@@ -14,8 +14,33 @@ Editable drawing code, native projects and shipped exports are collected under `
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/catalog/capabilities.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/capabilities.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/environments.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/environments.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/lighting.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/lighting.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/materials.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/materials.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/quality-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/quality-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| assets/render/catalog/scene-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/scene-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
| examples/material-card.json | SolidCraft contributors | original analytic box and explicitly synthetic engineering card; invented illustrative constants, no external material dataset | MIT OR Apache-2.0 |
| examples/studio/author_sweep.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/author_scene.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/materials.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded-sweep.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded-render.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-exploded.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
| examples/studio/stage1-sweep.json | SolidCraft contributors | original tangent circular-arc CAD cyclorama; no external assets | MIT OR Apache-2.0 |
| examples/studio/stage1.json | SolidCraft contributors | original editable CAD studio scene; no imported geometry or external image assets | MIT OR Apache-2.0 |
| examples/studio/stage1-render.json | SolidCraft contributors | original studio camera and lighting parameters | MIT OR Apache-2.0 |
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
| crates/gpu/src/path/denoise.wgsl | SolidCraft contributors | original bounded edge-aware a-trous spatial filter; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/denoise_guides.wgsl | SolidCraft contributors | original deterministic centre-ray material and geometry guide pass; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/shadows.wgsl | SolidCraft contributors | original bounded area, sun and environment visibility transport; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/path/textures.wgsl | SolidCraft contributors | original planar colour and normal map sampling; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
| assets/appearances/starter_pack.rs | SolidCraft contributors | original illustrative optical recipes, authored scalar values and future procedural resource descriptors; no measured or external material dataset | MIT OR Apache-2.0 |
| examples/material-finishes.json | SolidCraft contributors | original procedural sphere study of six illustrative opaque finishes with generic material densities; no external assets or measured optical data | MIT OR Apache-2.0 |
| examples/render-path-studio.json | SolidCraft contributors | original procedural opaque enclosure, teal insert and metal fasteners for progressive rendering; no imported models | MIT OR Apache-2.0 |
| crates/gpu/src/path/path.wgsl | SolidCraft contributors | original software BVH traversal and path integration using the existing SolidCraft surface/studio equations; no external shader source | MIT OR Apache-2.0 |
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
@@ -24,6 +49,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | retained editable historical VectorCraft source, with named face and background layers; redundant exports retired | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
@@ -74,6 +101,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -95,6 +123,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/appearances/opaque_finishes.rs | SolidCraft contributors | original illustrative opaque finish values; no external dataset, textures or measured engineering data | MIT OR Apache-2.0 |
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
@@ -107,7 +136,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -156,6 +185,20 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/gruvbox-light.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-light-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-light-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solidcraft-95.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/platinum-9.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/cupertino.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/green-phosphor.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/amber-phosphor.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/blueprint.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/one-dark.json | SolidCraft contributors | original SolidCraft theme file with colours adapted from the One Dark palette (Atom One Dark by GitHub Atom, https://github.com/atom/atom, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/tokyo-night.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Tokyo Night by enkia, https://github.com/enkia/tokyo-night-vscode-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solarized-light.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Solarized by Ethan Schoonover, https://github.com/altercation/solarized, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nord.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Nord by Sven Greb and the Nord contributors, https://github.com/nordtheme/nord, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/high-contrast.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
@@ -182,6 +225,11 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_keyboard.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_monitor.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_mouse.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_palette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_plug.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -319,6 +367,11 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/offset_symmetric.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/import_tray.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/export_tray.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/send.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -460,6 +513,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
| docs/sponsors/blacksmith-ci-powered.png | Blacksmith (blacksmith.sh) | "CI powered by Blacksmith" sponsor banner supplied by Blacksmith for the sponsorship credit; used only to credit the sponsor, not part of the application | © Blacksmith, used with permission |
| assets/icons/ui/design/svg/model/path_flange.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
## Code adapted from sibling projects
@@ -502,9 +556,9 @@ crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
## FreeCAD and Rhino import library
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
0.6.1 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
0.6.3 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
`cadmpeg-container`, `cadmpeg-codec-freecad`, `cadmpeg-codec-rhino` and `cadmpeg-codec-step`
in native builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
in native and browser builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
default features, so cadmpeg's codecs for commercial CAD formats and its ShapeManager/ACIS
codec are not compiled in. Its licence text is in THIRD-PARTY-LICENSES.txt; no code from it is
copied into SolidCraft.
@@ -537,3 +591,453 @@ A frame whose meshes the GPU refuses to stage (device lost or out of memory) is
skipped with a warning instead of panicking (#1469).
Paint callbacks are prepared only after a drawable is acquired, so a skipped
surface frame cannot strand cached meshes without their GPU initialization.
## Runtime studio lighting mathematics
`crates/gpu/src/path/path.wgsl` implements the power-heuristic estimator from the
mathematical description in [PBRT fourth edition, section 13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
No PBRT code or assets were copied. `crates/render/src/job/lighting.rs` evaluates
[Krzysztof Narkowicz's published 2016 ACES rational fit](https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/)
using its published coefficients (author licenses the fit under CC0/MIT).
These original implementations are under SolidCraft's MIT OR Apache-2.0 licence.
## Original coffee framing variant
| File | Author | Provenance | Licence |
|---|---|---|---|
| examples/studio/author_coffee_framing.py | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing-sweep.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing-render.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-framing.md | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
## Original cylindrical coffee housing variant
| File | Author | Provenance | Licence |
|---|---|---|---|
| examples/studio/author_coffee_cylinder.py | SolidCraft contributors | original parametric cylindrical CAD construction and editable command recipes; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder.json | SolidCraft contributors | original cylindrical housing on the original exploded coffee composition; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder-sweep.json | SolidCraft contributors | original cylindrical housing and continuous CAD cyclorama; no external assets | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder-render.json | SolidCraft contributors | original camera, studio lighting and optical settings retained from the framing variant | MIT OR Apache-2.0 |
| examples/studio/coffee-cylinder.md | SolidCraft contributors | original dimensional recipe, provenance and validation limitations | MIT OR Apache-2.0 |
## Runtime environment sampling
`crates/render/src/lighting/environment.rs` and the environment/sun portions of
`crates/gpu/src/path/path.wgsl` are original SolidCraft implementations of
solid-angle importance sampling and MIS, informed by the mathematical description
in [PBRT fourth edition, sections 12.5](https://pbr-book.org/4ed/Light_Sources/Infinite_Area_Lights)
and [13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
The procedural gradient and inline RGBE test fixtures are original mathematical
colours, not downloaded imagery. No third-party environment images or shader code
are included. Licence: MIT OR Apache-2.0.
## Original tumbler showcase assets
The CAD room, tumblers, window opening, wood/weave patterns and RGBE sky below
are original SolidCraft contributor work. No third-party scene, photograph, HDRI,
texture, or AI-generated artwork was used. Python authoring sources are included.
All listed files use the repository MIT OR Apache-2.0 licence.
| File | Source / author | Use | License |
| --- | --- | --- | --- |
| examples/studio/author_showcase.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/author_tumblers.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/ATTRIBUTION.md | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/author_textures.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/environment-manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/textures/manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-hdr-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-hdr-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-materials.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-sun.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
| examples/studio/tumblers.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
The original studio generators produce local ignored PNG maps and an RGBE gradient.
Generated image bytes are not source assets; their authorship, licences and expected
hashes are documented under `examples/studio/textures`. Rendered evidence belongs
on the results branch. No photographs, scans or external image assets are included.
## Additional render and showcase resources
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/starter/tools/check_all.py | SolidCraft contributors | original read-only authored/native resource validation orchestration and evidence receipts | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_check_all.py | SolidCraft contributors | original validation-runner regression fixtures and subprocess tests | MIT OR Apache-2.0 |
| assets/render/starter/tools/requirements.txt | SolidCraft contributors | pinned offline authoring tool requirements; no application dependency changes | MIT OR Apache-2.0 |
| crates/render/examples/validate_starter.rs | SolidCraft contributors | original native resource conformance checker using public SolidCraft decoding, normalization, sampling and mip APIs | MIT OR Apache-2.0 |
| .gitattributes | SolidCraft contributors | original canonical line-ending policy for the existing runtime icon-discovery manifest; no artwork or icon-assignment changes | MIT OR Apache-2.0 |
| assets/render/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
| docs/render-materials/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/assembly-bench-soft.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/automotive-turntable.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/black-metal-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/cnc-overhead.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/esd-bench-neutral.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/fdm-neutral-inspection.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/garage-door.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/hangar-skylight.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/home-workbench.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/makerspace-worklights.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/mechanical-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/neutral-comparison.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/neutral-side-softbox.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/paint-inspection-bands.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/printed-part-photo.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/rework-offset-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/robot-led-editorial.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/showroom-ribbons.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/spacecraft-clean-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/sun-free-overcast.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/vacuum-black.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/warm-table-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/white-enveloping-studio.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/exports/woodshop-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
| assets/render/starter/environments/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/environments/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/lighting/exports/rigs.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/lighting/recipe.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-001/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-002/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-003/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-004/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-005/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-006/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-007/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-008/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-009/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-010/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-011/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-012/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-013/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-014/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-015/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-016/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-017/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-018/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-019/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-020/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-021/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-022/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-023/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-024/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-025/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-026/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-027/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-028/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-029/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-030/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-031/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-032/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-033/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-034/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-035/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-036/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-037/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-038/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-039/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-040/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-041/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-042/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-043/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-044/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-045/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-046/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-047/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/exports/SURF-048/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
| assets/render/starter/materials/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/materials/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/pack.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
| assets/render/starter/tools/environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/test_materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/starter/tools/validate_pack.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/licenses/UFL.txt | Canonical Ltd. | exact Ubuntu Font Licence 1.0 notice retained from https://github.com/0xlillith/solidcraft-rover/blob/b32437915372d32b71419dc8f5143fe22ff1e869/UFL.txt for the modelled Ubuntu Light lettering | Ubuntu Font Licence 1.0 |
| assets/render/selene/materials/appearance-pack.json | SolidCraft contributors | original clean-room English appearance authoring data: 48 illustrative opaque PBR projections with separate advanced target specifications; no measured dataset or external assets | MIT OR Apache-2.0 |
| assets/render/selene/materials/appearance-swatches.json | SolidCraft contributors | original analytic command script for 48 opaque box swatches and a support base, derived only from the adjacent original appearance pack; no external artwork or engineering properties | MIT OR Apache-2.0 |
| assets/render/selene/models/SELENE-functional.solidcraft | SolidCraft contributors (native enhancements); 0xlillith (original rover); SychPL / Mateusz Sych (supplied showcase assembly); Canonical Ltd. (lettering outlines) | native-command derivative of the unchanged author-supplied source, with original bearing, suspension, gripper, electronics and presentation work; source and final hashes in assets/render/selene/SOURCE.md and VALIDATION.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
| assets/render/selene/motion/interference-pairs.json | SolidCraft contributors | original authored exact-body pair selection for bounded native clearance checks on the functional SELENE derivative; not a generated collision result | MIT OR Apache-2.0 |
| assets/render/selene/motion/mechanism-acceptance.json | SolidCraft contributors | original kinematic acceptance criteria, inspected source-container bindings and analytic planning checks from cited public SolidCraft documentation and the admitted native source; derivative runtime acceptance is recorded separately | MIT OR Apache-2.0 |
| assets/render/selene/motion/preflight-commands.json | SolidCraft contributors | original read-only inventory script using existing project commands; no source-model selectors or state mutations | MIT OR Apache-2.0 |
| assets/render/selene/motion/sample-sequence.json | SolidCraft contributors | original future lunar sampling and sealed-vial dynamics state specification; no simulation or physical calibration result | MIT OR Apache-2.0 |
| assets/render/selene/scenes/roles.json | SolidCraft contributors | original semantic role and candidate-material authoring specification for the prepared SELENE demo; no native geometry or external assets bundled | MIT OR Apache-2.0 |
| assets/render/selene/scenes/shots.json | SolidCraft contributors | original shot, framing and staged-sequence authoring specification for the prepared SELENE demo; no render or simulation result claimed | MIT OR Apache-2.0 |
| assets/render/selene/scenes/stages.json | SolidCraft contributors | original stage composition and light-ownership authoring specification derived from the project render catalog; scientific context linked in scenes/README.md | MIT OR Apache-2.0 |
| assets/render/selene/source-lock.json | SolidCraft contributors | original source-admission metadata and public-reference inventory pinning the supplied improved SELENE native file; original-model and font notices preserved in SOURCE.md | MIT OR Apache-2.0 |
| assets/render/selene/source/SELENE-showcase.solidcraft | 0xlillith (original rover); SychPL / Mateusz Sych (showcase assembly and helpers); SolidCraft contributors (emblem); Canonical Ltd. (lettering outlines) | unchanged author-published native file from https://github.com/SychPL/solidcraft/blob/32ec1720447e3fcc6bb6cc53a63fb5b39cfa21b9/SELENE-showcase.solidcraft; source hash and license evidence in assets/render/selene/SOURCE.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
| assets/render/selene/tools/bearings.py | SolidCraft contributors | original native-command authoring of grooved bearings, rolling-element kinematics, clamp hubs and arm/head clearances; analytic geometry, no copied CAD source | MIT OR Apache-2.0 |
| assets/render/selene/tools/build.py | SolidCraft contributors | original Python development tool for pinned-source native command replay, staged checkpoints, command-trace and geometry health checks; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/electronics.py | SolidCraft contributors | original native-command Raspberry Pi 5 visual reconstruction using public manufacturer mechanical dimensions; original package envelopes and connector internals, no manufacturer CAD or artwork copied | MIT OR Apache-2.0 |
| assets/render/selene/tools/export_views.py | SolidCraft contributors | original native MCP preview export tool for three reusable showcase views; changes only transient visibility and cameras and verifies the source master hash | MIT OR Apache-2.0 |
| assets/render/selene/tools/finishes.py | SolidCraft contributors | original native physical-material proxy and opaque PBR assignment generator preserving the supplied rover palette | MIT OR Apache-2.0 |
| assets/render/selene/tools/mechanisms.py | SolidCraft contributors | original native-command gripper sleeves, pads, wheel ownership, joint limits and inspection studies with independent analytic suspension relation | MIT OR Apache-2.0 |
| assets/render/selene/tools/prepare.py | SolidCraft contributors | original Python development tool for bounded native-container inspection and reviewed opaque body-assignment preparation; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/presentation.py | SolidCraft contributors | original native-command browser organization, camera/render setups and closed sample-vial glass preparation | MIT OR Apache-2.0 |
| assets/render/selene/tools/suspension.py | SolidCraft contributors | original native-command trailing-arm suspension, telescoping coilover loops, plain bushings, carrier gussets and local motion clearances derived from the admitted rover geometry; independent analytic pin-distance relation, no copied external CAD | MIT OR Apache-2.0 |
| assets/render/selene/tools/validate_native.py | SolidCraft contributors | original bounded native MCP verification tool for sampled SELENE kinematics, independent analytic follower and world-coordinate checks, optional exact-name interference pairs and unchanged input hashes; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/validate_prepared.py | SolidCraft contributors | original Python development validator for prepared SELENE resources, source status, references, hashes and command projections; no application runtime dependency | MIT OR Apache-2.0 |
| assets/render/selene/tools/yaw_supports.py | SolidCraft contributors | original native-command plain yaw journals, flanged brass thrust bushings, recessed axial retaining pins and mounting-head clearances derived from the admitted rover geometry; no copied external CAD | MIT OR Apache-2.0 |
| assets/render/selene/ui/demo-flow.json | SolidCraft contributors | original progressive-disclosure UI integration specification for the existing Render workspace; no copied UI artwork or runtime registration | MIT OR Apache-2.0 |
| docs/render-materials/baseline-setup-commands.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/capabilities.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/environment-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/environments.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/lighting.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/lunar-physics-fixtures.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/materials.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/provenance.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/quality-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/roadmap.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/scene-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/selene-bindings.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/simulation-requirements.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/studio-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/ui-integration.json | SolidCraft contributors | original English UI integration specification and exact source-revision mappings from SolidCraft's own implemented shell, UI kit and accepted icons; no copied artwork or runtime registration | MIT OR Apache-2.0 |
| docs/render-materials/ux.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
| docs/render-materials/validate_catalog.py | SolidCraft contributors | original Python development validator for catalog references, preset composition and analytic specification fixtures; no application runtime dependency | MIT OR Apache-2.0 |
### Built-in render resource adapters (personal integration batch, 2026-10-22)
The original adapters in `crates/engine/src/cmd/render/library.rs`, the generated lookup `library_data.rs` and its generator `tools/render-library/generate.py` project the already attributed starter and SELENE resources without replacing their bytes or authoring manifests. The added scalar texture channels, explicit color/V conventions and immutable-source/GPU-payload sharing are original contributor work under MIT OR Apache-2.0. No external code, commercial CAD data or new third-party imagery was imported. Source metadata and the original authoring attribution remain authoritative; pending runtime acceptance is not a photometric or engineering qualification.
## Physical reference resource additions
| File | Author | Source | Licence |
|---|---|---|---|
| assets/render/physical/bindings.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/exports/coverage.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/exports/native-cards.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/profiles.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/quantities.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/sources.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
| assets/render/physical/tools/check_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/test_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
| assets/render/physical/tools/export_library.py | SolidCraft contributors | original explicit reference-library planner and native no-loss export verifier; no external code | MIT OR Apache-2.0 |
| assets/render/physical/tools/test_export_library.py | SolidCraft contributors | original regressions for native capability refusal, source identity and physical-card preservation | MIT OR Apache-2.0 |
| tools/render-library/selene_studio.py | SolidCraft contributors | original bounded native MCP studio orchestration with pinned editable master, explicit GPU policy and request/export receipts; no generated model or image substitutes | MIT OR Apache-2.0 |
| tools/render-library/selene_recipe.py | SolidCraft contributors | original full-detail SELENE material/light composition and analytic camera fit; unchanged pinned native master, no external imagery or measured optical dataset | MIT OR Apache-2.0 |
| tools/render-library/test_selene_recipe.py | SolidCraft contributors | original independent projection, scaling, nonmutation and input-refusal fixtures for the SELENE studio recipe | MIT OR Apache-2.0 |
| assets/render/selene/scenes/studio-hero.json | SolidCraft contributors | original executable render intent and verified provenance for the selected native SELENE studio showcase; authored RGB appearance and relative light units, not measured optics or simulation results | MIT OR Apache-2.0 |
| tools/render-library/publish_selene.py | SolidCraft contributors | original bounded native-frame publication checks for explicit visual review, source identity, resource hashes, PNG CRCs and scanline limits; copies completed native bytes without image synthesis or resampling | MIT OR Apache-2.0 |
| tools/render-library/test_publish_selene.py | SolidCraft contributors | original tiny analytic PNG fixtures for source-size, CRC, expansion, dimensions and explicit-publication-consent regressions | MIT OR Apache-2.0 |
| tools/render-library/package_selene.py | SolidCraft contributors | original byte-preserving packaging of the reviewed native SELENE PNG, public provenance and checksums; bounded resource/PNG verification, no fonts, executables or new render execution | MIT OR Apache-2.0 |
| .github/workflows/selene-showcase.yml | SolidCraft contributors | original read-only artifact-delivery workflow for the reviewed native SELENE export; existing pinned repository actions, no compilation, package installation, secret use or renderer claims | MIT OR Apache-2.0 |
| tools/render-library/verify_linear_master.py | SolidCraft contributors | original independent validation driver for native linear EXR output using the official OpenEXR Python reader as an external test oracle; no codec implementation copied or production dependency added | MIT OR Apache-2.0 |
## Aging, effects and backgrounds showcase, 2026-10-23
| File | Author | Source | Licence |
|---|---|---|---|
| tools/render-showcase/instrument.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| tools/render-showcase/compose.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| tools/render-showcase/package.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
| distribution/SolidCraft-Render-Showcase.zip | SolidCraft contributors and the explicitly credited original model/UI/resource authors | byte-verified selected collaborator package of reviewed native media, editable sample, recipes and public provenance; contained attribution and existing SELENE lettering notices retained; no fonts or executable binaries | MIT OR Apache-2.0 for new contributions; referenced original notices retained |
| distribution/SolidCraft-Render-Showcase.json | SolidCraft contributors | original verified delivery archive manifest with exact SHA-256, member count and bounded scope | MIT OR Apache-2.0 |
+24 -2
View File
@@ -15,6 +15,15 @@ Before choosing a task, check the [command inventory](docs/features.md) and
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
## Kernel work is paused
We're evaluating a new geometry kernel. Until that's decided, please don't open issues, pull
requests or discussions about kernel work: booleans, fillets, chamfers, shell, offsets, sweeps and
lofts, threads and holes, STEP/IGES import and export, tessellation, or anything else that changes
`crates/kernel/`. The pinned issue on the tracker explains it. New ones get the `kernel-paused`
label (`.github/workflows/kernel-paused.yml`) and a maintainer answers them. Crashes and data loss
are still taken as issues, and everything outside the kernel carries on as normal.
## Get it running (about 10 minutes on a fast machine)
You need stable Rust 1.95 or newer (required by egui/eframe 0.36). For the web
@@ -177,8 +186,14 @@ no LTO, and debug assertions off (truck has debug-only assertions that reject va
builds several times faster than `--release`. If a test fails only in a plain debug build, say so
in the PR. `cargo xtask test` builds the tests with it and then runs every test binary at once
(`cargo test` runs them one after another); arguments after `--` go to each binary, e.g.
`cargo xtask test -- shell`. Tests run beside each other on a busy machine, so a test must not
depend on wall-clock timing.
`cargo xtask test -- shell`. Set `SOLIDCRAFT_TEST_JOBS` to a positive integer to opt into a
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
worker consumes one of those slots. Every binary and side check still runs after failures.
Without the variable the existing concurrency is unchanged. `RUST_TEST_THREADS`,
`ACTION_MATRIX_JOBS` and Cargo's build-job setting remain independent caller overrides;
this limit does not choose their values or change test filters, timeouts or baselines.
Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
runs Clippy, the test build and tests for those and their dependents. The
@@ -265,6 +280,9 @@ and a fresh CI run:
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
features`). Don't regenerate or edit them in a pull request.
- **Design format**: one file per change, never an edit to a shared number (above).
- **New UI strings and attribution rows**: one new file per change, `assets/locales/pending/<slug>.json`
and `attribution/pending/<slug>.md`, instead of editing the catalogs or the ATTRIBUTION.md table;
`cargo xtask fold-pending` folds them on `main` (see the README in each folder).
## Sending a change
@@ -331,6 +349,10 @@ owning one area, with a human setting direction and reporting bugs. Their workin
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
long as the change meets the rules above.
Two quick checks run on every pull request (`.github/workflows/pr-checks.yml`, not required): it
fails when a lower-numbered open PR by someone else already closes the same issue (the label
`not-a-duplicate` skips it), or when the description lacks the `Changes a default: no` / `Changes a default: yes — <what>, old behaviour kept behind preference <name>` line (docs-only and bot PRs are exempt).
## Labels and triage
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
Generated
+102 -98
View File
@@ -590,9 +590,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadmpeg-codec-freecad"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a5257265c2549cf59a8457b3c7aadcc96c95abe20205232553766993a808812"
checksum = "3c4a8fa61748cbb3ce80838b00a8cebfe553460583941f5aba6e2b97c16b4d37"
dependencies = [
"cadmpeg-container",
"cadmpeg-core",
@@ -601,14 +601,14 @@ dependencies = [
"serde",
"serde_json",
"tempfile",
"zip 8.6.0",
"zip",
]
[[package]]
name = "cadmpeg-codec-rhino"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d88b42fb91c7e5af4c75c0433029771f7e3233a0b72ec3d0b2e5ff2c72bed07"
checksum = "d7af41440d4c1d69e88e612de02cc3c9bad79d03429531e640ac924fef53c6bf"
dependencies = [
"cadmpeg-container",
"cadmpeg-core",
@@ -625,53 +625,53 @@ dependencies = [
[[package]]
name = "cadmpeg-codec-step"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83a52258c129a683f77fb360ca30af54ae34339625ad890be12621128a5811c"
checksum = "35e1b6d17cf26b31f59f2edab8ef5c29d2a33216c581fd913f012ad4869bd559"
dependencies = [
"base64",
"cadmpeg-container",
"cadmpeg-core",
"cadmpeg-ir",
"encoding_rs",
"serde",
"serde_json",
"thiserror 2.0.21",
"zip 8.6.0",
"zip",
]
[[package]]
name = "cadmpeg-container"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c110508cd9155e905f52906de505dfd42dc70a0321f5e90b5f68c28ba207686e"
checksum = "5ab7bcef48ac7480b4917820272d85cd2c9533e17904e15b0e4d22f19a6045bd"
dependencies = [
"cadmpeg-core",
"crc32fast",
"flate2",
"zip 8.6.0",
"zstd 0.14.2",
"zip",
]
[[package]]
name = "cadmpeg-core"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b1147e51a74e7c52da5b393a0344597238cdc622fac6443eb2fd526759f6fa"
checksum = "e86bb7a40521832504bb1f5953a1c706b2c52aad73bf6d84e1b35e5cb0beae3f"
dependencies = [
"flate2",
"memchr",
"roxmltree",
"ruzstd",
"serde",
"serde_json",
"thiserror 2.0.21",
"zstd 0.14.2",
]
[[package]]
name = "cadmpeg-ir"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ff19f019f013d97e353bb802c5d2e253c2859aaa5422009aa933dc2f2e7dbc"
checksum = "b570a1a8c579e407a9fd7bb10fa1900f8827f1d578b8fcb4ea2a056b5f323c72"
dependencies = [
"base64",
"cadmpeg-core",
@@ -684,9 +684,9 @@ dependencies = [
[[package]]
name = "cadmpeg-registry"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66046f1a694926707bea8944f861e699964b04c68972708588f97e14ccee3c5d"
checksum = "b2be23469a9bca0b684f1811118fffb581ebd32ded08e6cc8b0716a41f8c1377"
dependencies = [
"cadmpeg-codec-freecad",
"cadmpeg-codec-rhino",
@@ -996,6 +996,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
@@ -2271,6 +2280,16 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
dependencies = [
"bitflags 2.13.2",
"serde",
]
[[package]]
name = "keyring"
version = "3.6.3"
@@ -2480,6 +2499,24 @@ dependencies = [
"pxfm",
]
[[package]]
name = "muda"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
dependencies = [
"crossbeam-channel",
"dpi",
"keyboard-types",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"png",
"thiserror 2.0.21",
"windows-sys 0.61.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -3749,6 +3786,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ruzstd"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4ce4a039f42bf25920b95667e4d789e98749ab11bbb8d2c2936f788421bb5"
dependencies = [
"twox-hash",
]
[[package]]
name = "same-file"
version = "1.0.6"
@@ -4162,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4172,6 +4218,7 @@ dependencies = [
"hidapi",
"keyring",
"log",
"muda",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
@@ -4193,13 +4240,14 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.18.2"
version = "0.22.1"
[[package]]
name = "solidcraft-cli"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"image",
"log",
"serde_json",
"solidcraft-engine",
"solidcraft-gpu",
@@ -4208,7 +4256,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4217,7 +4265,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"log",
"serde",
@@ -4234,7 +4282,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"log",
"schemars",
@@ -4248,12 +4296,13 @@ dependencies = [
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
"toml_edit",
"web-time",
]
[[package]]
name = "solidcraft-geom"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"serde",
"serde_json",
@@ -4262,15 +4311,18 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"image",
"log",
"naga",
"pollster 0.4.0",
"robust",
"serde",
"serde_json",
"solidcraft-engine",
"solidcraft-geom",
"solidcraft-io",
"solidcraft-kernel",
"solidcraft-render",
"thiserror 2.0.21",
@@ -4279,13 +4331,14 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
"cadmpeg-registry",
"chardetng",
"encoding_rs",
"gethostname",
"gltf",
"log",
"quick-xml",
@@ -4297,12 +4350,12 @@ dependencies = [
"solidcraft-geom",
"solidcraft-kernel",
"thiserror 2.0.21",
"zip 9.0.4",
"zip",
]
[[package]]
name = "solidcraft-kernel"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"log",
"proptest",
@@ -4322,7 +4375,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4331,7 +4384,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"image",
"moxcms",
@@ -4345,7 +4398,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"serde",
"serde_json",
@@ -4356,14 +4409,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"arboard",
"egui",
@@ -4371,7 +4424,9 @@ dependencies = [
"image",
"log",
"minisign-verify",
"nix 0.31.3",
"pollster 0.4.0",
"proc-macro2",
"semver",
"serde",
"serde_json",
@@ -4385,12 +4440,12 @@ dependencies = [
"ttf-parser",
"web-time",
"winreg",
"zip 9.0.4",
"zip",
]
[[package]]
name = "solidcraft-web"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"eframe",
"egui",
@@ -4645,6 +4700,7 @@ dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
@@ -4839,6 +4895,12 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "twox-hash"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
[[package]]
name = "type-map"
version = "0.5.1"
@@ -5920,10 +5982,12 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.18.2"
version = "0.22.1"
dependencies = [
"proc-macro2",
"serde_json",
"sha2 0.10.9",
"syn 2.0.119",
]
[[package]]
@@ -6211,20 +6275,6 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "zip"
version = "8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
dependencies = [
"crc32fast",
"flate2",
"indexmap",
"memchr",
"typed-path",
"zstd 0.13.3",
]
[[package]]
name = "zip"
version = "9.0.4"
@@ -6264,52 +6314,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe 7.3.0",
]
[[package]]
name = "zstd"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057cfd910cfac363a0ada849592624b4c9ff2e10bef504c3433810d78ed96f93"
dependencies = [
"zstd-safe 8.1.0",
]
[[package]]
name = "zstd-safe"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-safe"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd44c6a7284e91f3717755b24315a302edd9153a01f753c3cba3d765e8eafac"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.1.1+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.5.3"
+12 -5
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.18.2"
version = "0.22.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -13,6 +13,7 @@ authors = ["SolidCraft contributors"]
repository = "https://github.com/bherbruck/solidcraft"
[workspace.dependencies]
robust = "1.2"
# internal
solidcraft-geom = { path = "crates/geom" }
solidcraft-sketch = { path = "crates/sketch" }
@@ -66,18 +67,24 @@ encoding_rs = "0.8"
# glTF 2.0 import (MIT OR Apache-2.0): the document model and GLB container only; buffers are
# read by crates/io itself, so no image decoding or file access comes with it.
gltf = { version = "1.4", default-features = false, features = ["names"] }
# The computer's name in a design's "open elsewhere" lock file (MIT OR Apache-2.0; already in the
# tree through x11rb).
gethostname = "1"
# FreeCAD and Rhino import (Apache-2.0, pure Rust apart from zstd; native builds only).
# FreeCAD and Rhino import (Apache-2.0, pure Rust; native and browser builds).
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
# features, so cadmpeg's codecs for commercial formats (f3d, sldprt, inventor, catia, creo, nx)
# and its ShapeManager/ACIS `sat` codec are not compiled in. `step` is the writer we restate through.
cadmpeg-core = "=0.6.1"
cadmpeg-ir = "=0.6.1"
cadmpeg-registry = { version = "=0.6.1", default-features = false, features = ["fcstd", "rhino", "step"] }
cadmpeg-core = "=0.6.3"
cadmpeg-ir = "=0.6.3"
cadmpeg-registry = { version = "=0.6.3", default-features = false, features = ["fcstd", "rhino", "step"] }
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
# Ends a process group with the system call (MIT; already in the tree through zip).
nix = { version = "0.31", default-features = false, features = ["signal"] }
# images
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
png = "0.18.1"
+51 -5
View File
@@ -51,26 +51,37 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|---|---|---|---|
| M0 | Vertical slice | done | — |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces, at distances from reference edges, and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), drawing snaps to where two lines, circles or arcs cross (#4967), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop, browser and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; Browser Expand All / Collapse All (`browser.expand_all`, `browser.collapse_all`) on the design and component menus; preferences | in progress | 6 |
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements, and exported as an animation coming apart and back together); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. macOS Developer ID signing and notarization are in the release workflow (#5749; they switch on when the Apple secrets exist). Windows installer signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
| M15 | Frames and weldments (#4003): structural members along 2D and 3D sketch lines, butt and mitre corners, trim to faces and bodies, end caps, gussets, cosmetic welds and a cut list. Done: the profile library (`frame.profile.list`, `frame.profile.properties`, `frame.profile.insert`): bar, round bar, rectangular and round tube, angle, channel and I-beam, each SolidCraft's own parametric definition with exact fillet arcs and exact section properties (area, centroid, second moments), tested against hand-computed values; members (`frame.member`): one body per straight line of a 2D or 3D frame sketch, following parameter edits, with a built-in profile (its dimensions parameter expressions) or a profile sketch (its drawn material by alternating nested loops), anchor, angle and mirror; each member's name kept with its line. Left: corners, trims, end caps, gussets, welds, cut list, FRAME tab and dialogs | in progress | 60 |
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
With the **Fusion** preset and two-finger pan enabled, two fingers pan and Shift + two fingers
orbit freely, with pinch zoom and mouse bindings unchanged. The shared navigation handler
applies the same gestures to model and Render views and blocks orbit on flat pages.
M11 precursor: File → Export Current View as DXF exports shown placed bodies from the current
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
action tiles, recent rows and sample cards, with recovery review and Save As/Export
entry points. Document loading, sample caches and recovery safety retain their
existing command paths.
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
hierarchies are shared; bounded desktop preparation and occlusion use up to eight workers.
@@ -134,6 +145,12 @@ completed drawings publish atomically. The browser still exports synchronously.
## Render foundation
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
- Definition-local visual mapping (#1214) supplies a bounded numerical frame, physical repeat coordinates and a deterministic one-time grain-axis suggestion. It adds no saved record or texture consumer; see [the mapping contract](docs/render-visual-mapping.md).
- Explicit texture normalization (#1218) produces completed float RGBA with
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
@@ -189,6 +206,11 @@ completed drawings publish atomically. The browser still exports synchronously.
## Dimensioning and error reporting
- Operation length fields share design-unit defaults and input evaluation, including secondary
distances, thin walls, blend controls, sheet/plastic rules, joints and motion keys. Canvas
handles and sketch previews use the same lengths as committed geometry. Existing stored
millimetre values retain their size when edited in a design using another unit (#3271).
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
Both tools retain full replies by default.
@@ -232,6 +254,9 @@ completed drawings publish atomically. The browser still exports synchronously.
- Sketch Dimension takes a line and a circle or arc (#1803): it measures from the line to the
circle's edge (the gap, 0 when tangent, or how far the edge reaches past a line that cuts
it), drives it like any other dimension and keeps the circle on its side of the line.
- Sketch Dimension takes a point and a circle or arc, or two circles or arcs (#4064): it
measures to the circle's edge (from the point, or between the two edges: the gap, 0 when
they touch, how far they overlap, or the narrowest gap between nested circles).
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
the current design unit; changing units leaves existing geometry and expressions intact.
@@ -329,8 +354,25 @@ completed drawings publish atomically. The browser still exports synchronously.
- Saved projections from a removed strip retain their geometry and report the lost
reference. Explicitly reselect the wall to repair that projection.
## Workspace layouts (#2029)
- General preferences save Floating or Compact using the universal UI containers.
Compact joins the workspace selector and ribbon, docks the content-hugging Browser
flush beneath them with a square join, and opens tool sheets below the compass.
Sheets remain draggable, rounding all corners away from their right dock; footer
backgrounds follow the same corners. A separate preference hides ribbon icon labels
for a shorter ribbon in either layout.
Assistant keeps the outermost edge and History keeps its chosen dock. Switching
retains the active tool, camera, selection and design; system windows and menus
keep their existing behavior.
## View navigation
- Navigation has an opt-in Free orbit preference for mouse and 3D-mouse rotation through
both poles with roll. It defaults off, preserving existing preset snapping and pole limits.
The SolidWorks face menu offers Normal to Selection (`view.normal_to`), shared with Look At
and Ctrl+8, including repeat-to-flip, preserving scale and projection.
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
its layout stable during a drag and applies the selected scale on release.
@@ -561,6 +603,10 @@ completed drawings publish atomically. The browser still exports synchronously.
continuous collision detection; local thin walls and small details remain limitations.
See [contact-set limits and recovery](docs/contact-sets.md).
## Assembly dragging
The design's first part is grounded when it is made, and a left-button drag repositions a free part (`occurrence.drag`: one undo step, exact occurrence paths including repeated jointed definitions, any floating-chain member carries the chain, grounded parts and joints hold where present, Escape cancels).
## 3D sketch bridges
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
+767 -126
View File
@@ -1,6 +1,6 @@
Third-party software in SolidCraft
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 559 crates below.
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
@@ -60,13 +60,13 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
byteorder 1.5.0 (Unlicense OR MIT)
byteorder-lite 0.1.0 (Unlicense OR MIT)
bytes 1.12.1 (MIT)
cadmpeg-codec-freecad 0.6.1 (Apache-2.0)
cadmpeg-codec-rhino 0.6.1 (Apache-2.0)
cadmpeg-codec-step 0.6.1 (Apache-2.0)
cadmpeg-container 0.6.1 (Apache-2.0)
cadmpeg-core 0.6.1 (Apache-2.0)
cadmpeg-ir 0.6.1 (Apache-2.0)
cadmpeg-registry 0.6.1 (Apache-2.0)
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
cadmpeg-codec-step 0.6.3 (Apache-2.0)
cadmpeg-container 0.6.3 (Apache-2.0)
cadmpeg-core 0.6.3 (Apache-2.0)
cadmpeg-ir 0.6.3 (Apache-2.0)
cadmpeg-registry 0.6.3 (Apache-2.0)
calloop 0.13.0 (MIT)
calloop 0.14.5 (MIT)
calloop-wayland-source 0.3.0 (MIT)
@@ -95,6 +95,7 @@ core_detect 1.0.0 (MIT/Apache-2.0)
cpufeatures 0.2.17 (MIT OR Apache-2.0)
cpufeatures 0.3.1 (MIT OR Apache-2.0)
crc32fast 1.5.2 (MIT OR Apache-2.0)
crossbeam-channel 0.5.17 (MIT OR Apache-2.0)
crossbeam-deque 0.8.8 (MIT OR Apache-2.0)
crossbeam-epoch 0.9.21 (MIT OR Apache-2.0)
crossbeam-utils 0.8.23 (MIT OR Apache-2.0)
@@ -210,6 +211,7 @@ jni-sys 0.4.1 (MIT OR Apache-2.0)
jni-sys-macros 0.4.1 (MIT OR Apache-2.0)
js-sys 0.3.106 (MIT OR Apache-2.0)
katexit 0.1.5 (MIT OR Apache-2.0)
keyboard-types 0.8.3 (MIT OR Apache-2.0)
keyring 3.6.3 (MIT OR Apache-2.0)
khronos-egl 6.0.0 (MIT/Apache-2.0)
kurbo 0.13.1 (Apache-2.0 OR MIT)
@@ -234,6 +236,7 @@ minisign-verify 0.3.0 (MIT)
miniz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0)
miniz_oxide 0.9.1 (MIT OR Zlib OR Apache-2.0)
moxcms 0.8.1 (BSD-3-Clause OR Apache-2.0)
muda 0.21.2 (Apache-2.0 OR MIT)
multiversion_no_op 1.0.0 (Apache-2.0 OR MIT)
naga 30.0.1 (MIT OR Apache-2.0)
naga-types 30.0.1 (MIT OR Apache-2.0)
@@ -352,6 +355,7 @@ rustls-platform-verifier 0.7.1 (MIT OR Apache-2.0)
rustls-platform-verifier-android 0.2.0 (MIT OR Apache-2.0)
rustls-webpki 0.103.15 (ISC)
rustversion 1.0.23 (MIT OR Apache-2.0)
ruzstd 0.9.1 (MIT)
schannel 0.1.29 (MIT)
schemars 1.2.3 (MIT)
schemars_derive 1.2.3 (MIT)
@@ -429,6 +433,7 @@ truck-shapeops 0.4.0 (Apache-2.0)
truck-stepio 0.3.0 (Apache-2.0)
truck-topology 0.6.0 (Apache-2.0)
ttf-parser 0.25.1 (MIT OR Apache-2.0)
twox-hash 2.1.5 (MIT)
type-map 0.5.1 (MIT/Apache-2.0)
typed-path 0.12.3 (MIT OR Apache-2.0)
typeid 1.0.3 (MIT OR Apache-2.0)
@@ -546,16 +551,10 @@ zeroize_derive 1.5.0 (Apache-2.0 OR MIT)
zerotrie 0.2.5 (Unicode-3.0)
zerovec 0.11.8 (Unicode-3.0)
zerovec-derive 0.11.6 (Unicode-3.0)
zip 8.6.0 (MIT)
zip 9.0.4 (MIT)
zlib-rs 0.6.8 (Zlib)
zmij 1.0.23 (MIT)
zopfli 0.8.3 (Apache-2.0)
zstd 0.13.3 (MIT)
zstd 0.14.2 (BSD-3-Clause)
zstd-safe 7.3.0 (BSD-3-Clause)
zstd-safe 8.1.0 (BSD-3-Clause)
zstd-sys 2.1.1+zstd.1.5.7 (BSD-3-Clause)
zune-core 0.5.3 (MIT OR Apache-2.0 OR Zlib)
zune-jpeg 0.5.15 (MIT OR Apache-2.0 OR Zlib)
zvariant 4.2.0 (MIT)
@@ -1108,26 +1107,27 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
4.7.1, atomic-waker 1.1.2, base64 0.23.1, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo
3.20.3, cfg-if 1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
1.0.0, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, dbus-secret-service
4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14,
event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1, gltf-derive
1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna
1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106,
khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14,
log 0.4.34, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46,
num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0,
parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5,
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
security-framework-sys 2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2,
smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1,
unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi
0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures
0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129,
web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys
0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1,
linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8,
num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19,
once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5,
parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling
3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust
1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width
0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
wit-bindgen 0.57.1
================================================================================
Apache License
@@ -4030,7 +4030,605 @@ Apache License
limitations under the License.
================================================================================
crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
crossbeam-channel 0.5.17
================================================================================
===============================================================================
matching.go
https://creativecommons.org/licenses/by/3.0/legalcode
Creative Commons Legal Code
Attribution 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the Work and
other pre-existing works, such as a translation, adaptation,
derivative work, arrangement of music or other alterations of a
literary or artistic work, or phonogram or performance and includes
cinematographic adaptations or any other form in which the Work may be
recast, transformed, or adapted including in any form recognizably
derived from the original, except that a work that constitutes a
Collection will not be considered an Adaptation for the purpose of
this License. For the avoidance of doubt, where the Work is a musical
work, performance or phonogram, the synchronization of the Work in
timed-relation with a moving image ("synching") will be considered an
Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as
encyclopedias and anthologies, or performances, phonograms or
broadcasts, or other works or subject matter other than works listed
in Section 1(f) below, which, by reason of the selection and
arrangement of their contents, constitute intellectual creations, in
which the Work is included in its entirety in unmodified form along
with one or more other contributions, each constituting separate and
independent works in themselves, which together are assembled into a
collective whole. A work that constitutes a Collection will not be
considered an Adaptation (as defined above) for the purposes of this
License.
c. "Distribute" means to make available to the public the original and
copies of the Work or Adaptation, as appropriate, through sale or
other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or entities that
offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic work,
the individual, individuals, entity or entities who created the Work
or if no individual or entity can be identified, the publisher; and in
addition (i) in the case of a performance the actors, singers,
musicians, dancers, and other persons who act, sing, deliver, declaim,
play in, interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the producer
being the person or legal entity who first fixes the sounds of a
performance or other sounds; and, (iii) in the case of broadcasts, the
organization that transmits the broadcast.
f. "Work" means the literary and/or artistic work offered under the terms
of this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book,
pamphlet and other writing; a lecture, address, sermon or other work
of the same nature; a dramatic or dramatico-musical work; a
choreographic work or entertainment in dumb show; a musical
composition with or without words; a cinematographic work to which are
assimilated works expressed by a process analogous to cinematography;
a work of drawing, painting, architecture, sculpture, engraving or
lithography; a photographic work to which are assimilated works
expressed by a process analogous to photography; a work of applied
art; an illustration, map, plan, sketch or three-dimensional work
relative to geography, topography, architecture or science; a
performance; a broadcast; a phonogram; a compilation of data to the
extent it is protected as a copyrightable work; or a work performed by
a variety or circus performer to the extent it is not otherwise
considered a literary or artistic work.
g. "You" means an individual or entity exercising rights under this
License who has not previously violated the terms of this License with
respect to the Work, or who has received express permission from the
Licensor to exercise rights under this License despite a previous
violation.
h. "Publicly Perform" means to perform public recitations of the Work and
to communicate to the public those public recitations, by any means or
process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a
place individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of the
performances of the Work, including by public digital performance; to
broadcast and rebroadcast the Work by any means including signs,
sounds or images.
i. "Reproduce" means to make copies of the Work by any means including
without limitation by sound or visual recordings and the right of
fixation and reproducing fixations of the Work, including storage of a
protected performance or phonogram in digital form or other electronic
medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
limit, or restrict any uses free from copyright or rights arising from
limitations or exceptions that are provided for in connection with the
copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License,
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
perpetual (for the duration of the applicable copyright) license to
exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more
Collections, and to Reproduce the Work as incorporated in the
Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation,
including any translation in any medium, takes reasonable steps to
clearly label, demarcate or otherwise identify that changes were made
to the original Work. For example, a translation could be marked "The
original work was translated from English to Spanish," or a
modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated
in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme cannot be waived, the Licensor
reserves the exclusive right to collect such royalties for any
exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme can be waived, the Licensor waives the
exclusive right to collect such royalties for any exercise by You
of the rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right to
collect royalties, whether individually or, in the event that the
Licensor is a member of a collecting society that administers
voluntary licensing schemes, via that society, from any exercise
by You of the rights granted under this License.
The above rights may be exercised in all media and formats whether now
known or hereafter devised. The above rights include the right to make
such modifications as are technically necessary to exercise the rights in
other media and formats. Subject to Section 8(f), all rights not expressly
granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly made
subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms
of this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms
on the Work that restrict the terms of this License or the ability of
the recipient of the Work to exercise the rights granted to that
recipient under the terms of the License. You may not sublicense the
Work. You must keep intact all notices that refer to this License and
to the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or Publicly
Perform the Work, You may not impose any effective technological
measures on the Work that restrict the ability of a recipient of the
Work from You to exercise the rights granted to that recipient under
the terms of the License. This Section 4(a) applies to the Work as
incorporated in a Collection, but this does not require the Collection
apart from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any Licensor You
must, to the extent practicable, remove from the Collection any credit
as required by Section 4(b), as requested. If You create an
Adaptation, upon notice from any Licensor You must, to the extent
practicable, remove from the Adaptation any credit as required by
Section 4(b), as requested.
b. If You Distribute, or Publicly Perform the Work or any Adaptations or
Collections, You must, unless a request has been made pursuant to
Section 4(a), keep intact all copyright notices for the Work and
provide, reasonable to the medium or means You are utilizing: (i) the
name of the Original Author (or pseudonym, if applicable) if supplied,
and/or if the Original Author and/or Licensor designate another party
or parties (e.g., a sponsor institute, publishing entity, journal) for
attribution ("Attribution Parties") in Licensor's copyright notice,
terms of service or by other reasonable means, the name of such party
or parties; (ii) the title of the Work if supplied; (iii) to the
extent reasonably practicable, the URI, if any, that Licensor
specifies to be associated with the Work, unless such URI does not
refer to the copyright notice or licensing information for the Work;
and (iv) , consistent with Section 3(b), in the case of an Adaptation,
a credit identifying the use of the Work in the Adaptation (e.g.,
"French translation of the Work by Original Author," or "Screenplay
based on original Work by Original Author"). The credit required by
this Section 4 (b) may be implemented in any reasonable manner;
provided, however, that in the case of a Adaptation or Collection, at
a minimum such credit will appear, if a credit for all contributing
authors of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits for the
other contributing authors. For the avoidance of doubt, You may only
use the credit required by this Section for the purpose of attribution
in the manner set out above and, by exercising Your rights under this
License, You may not implicitly or explicitly assert or imply any
connection with, sponsorship or endorsement by the Original Author,
Licensor and/or Attribution Parties, as appropriate, of You or Your
use of the Work, without the separate, express prior written
permission of the Original Author, Licensor and/or Attribution
Parties.
c. Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any
Adaptations or Collections, You must not distort, mutilate, modify or
take other derogatory action in relation to the Work which would be
prejudicial to the Original Author's honor or reputation. Licensor
agrees that in those jurisdictions (e.g. Japan), in which any exercise
of the right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion, mutilation,
modification or other derogatory action prejudicial to the Original
Author's honor and reputation, the Licensor will waive or not assert,
as appropriate, this Section, to the fullest extent permitted by the
applicable national law, to enable You to reasonably exercise Your
right under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this License.
Individuals or entities who have received Adaptations or Collections
from You under this License, however, will not have their licenses
terminated provided such individuals or entities remain in full
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is
perpetual (for the duration of the applicable copyright in the Work).
Notwithstanding the above, Licensor reserves the right to release the
Work under different license terms or to stop distributing the Work at
any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is
required to be, granted under the terms of this License), and this
License will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
offers to the recipient a license to the original Work on the same
terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and
enforceable.
d. No term or provision of this License shall be deemed waived and no
breach consented to unless such waiver or consent shall be in writing
and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with
respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that
may appear in any communication from You. This License may not be
modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention
for the Protection of Literary and Artistic Works (as amended on
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
and the Universal Copyright Convention (as revised on July 24, 1971).
These rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be enforced
according to the corresponding provisions of the implementation of
those treaty provisions in the applicable national law. If the
standard suite of rights granted under applicable copyright law
includes additional rights not granted under this License, such
additional rights are deemed to be included in the License; this
License is not intended to restrict the license of any rights under
applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not be
liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder, it
shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of doubt,
this trademark restriction does not form part of this License.
Creative Commons may be contacted at https://creativecommons.org/.
===============================================================================
The Go Programming Language
https://golang.org/LICENSE
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
The Rust Programming Language
https://github.com/rust-lang/rust/blob/master/LICENSE-MIT
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
===============================================================================
The Rust Programming Language
https://github.com/rust-lang/rust/blob/master/LICENSE-APACHE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================================
crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
================================================================================
The MIT License (MIT)
@@ -7716,6 +8314,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
keyboard-types 0.8.3
================================================================================
Copyright (c) 2017 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
keyring 3.6.3
================================================================================
@@ -9452,6 +10074,56 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
muda 0.21.2
================================================================================
MIT License
Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
muda 0.21.2
================================================================================
SPDXVersion: SPDX-2.1
DataLicense: CC0-1.0
PackageName: muda
DataFormat: SPDXRef-1
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
PackageHomePage: https://tauri.app
PackageLicenseDeclared: Apache-2.0
PackageLicenseDeclared: MIT
PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy
PackageSummary: <text>Menu Utilities for Desktop Applications.
</text>
PackageComment: <text>The package includes the following libraries; see
Relationship information.
</text>
Created: 2022-12-05T09:00:00Z
PackageDownloadLocation: git://github.com/tauri-apps/muda
PackageDownloadLocation: git+https://github.com/tauri-apps/muda.git
PackageDownloadLocation: git+ssh://github.com/tauri-apps/muda.git
Creator: Person: Daniel Thompson-Yvetot
================================================================================
multiversion_no_op 1.0.0
================================================================================
@@ -11781,6 +12453,32 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The files under third-party/chromium are licensed as described in
third-party/chromium/LICENSE.
================================================================================
ruzstd 0.9.1
================================================================================
MIT License
Copyright (c) 2019 Moritz Borcherding
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
schannel 0.1.29
================================================================================
@@ -13042,6 +13740,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
twox-hash 2.1.5
================================================================================
The MIT License (MIT)
Copyright (c) 2015 Jake Goulding
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
type-map 0.5.1
================================================================================
@@ -15663,7 +16387,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
zip 8.6.0, zip 9.0.4
zip 9.0.4
================================================================================
The MIT License (MIT)
@@ -15918,89 +16642,6 @@ Apache License
See the License for the specific language governing permissions and
limitations under the License.
================================================================================
zstd 0.13.3
================================================================================
The MIT License (MIT)
Copyright (c) 2016 Alexandre Bury
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
zstd 0.14.2, zstd-safe 7.3.0, zstd-safe 8.1.0, zstd-sys 2.1.1+zstd.1.5.7
================================================================================
BSD 3-Clause License
Copyright (c) 2026, Alexandre Bury
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
zstd-sys 2.1.1+zstd.1.5.7
================================================================================
The auto-generated bindings are under the 3-clause BSD license:
BSD License
For Zstandard software
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook, nor Meta, nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
zune-core 0.5.3, zune-jpeg 0.5.15
================================================================================
@@ -16068,13 +16709,13 @@ accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
array-macro 2.1.8: MIT OR Apache-2.0 (https://codeberg.org/xfix/array-macro)
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
cadmpeg-codec-freecad 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-rhino 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-step 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-container 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-core 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-ir 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-registry 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
convert_case 0.4.0: MIT (https://github.com/rutrum/convert-case)
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
+6
View File
@@ -19,10 +19,16 @@ path = "src/main.rs"
name = "solvecraft-cli"
path = "src/alias.rs"
[features]
default = ["raytrace-cpu"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
[dependencies]
solidcraft-engine = { workspace = true }
solidcraft-gpu = { workspace = true }
solidcraft-mcp = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
@@ -0,0 +1,151 @@
//! Reproducible evidence through the production command/job interfaces.
//! Usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png
//! Writes the editable design, command timings and actual adapter/job metadata beside PNG.
//! Accepts runtime requests as well as inline or named saved render jobs.
use serde_json::{Value, json};
use solidcraft_engine::{
Session,
doc::render_settings::{RenderJob, RenderSetup},
render::job::{CpuHost, ErrorCode, JobId, Phase},
};
use std::{
io::Read,
path::Path,
time::{Duration, Instant},
};
fn read_json(path: &str) -> Result<Value, String> {
let mut bytes = Vec::new();
std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?.take(16_777_217).read_to_end(&mut bytes).map_err(|e| e.to_string())?;
if bytes.len() > 16_777_216 {
return Err("evidence input exceeds 16 MiB".into());
}
serde_json::from_slice(&bytes).map_err(|e| format!("{path}: {e}"))
}
// Only decide which optional host must be installed. The production command
// still validates and resolves the actual request without a second wire format.
fn needs_gpu(session: &Session, params: &Value) -> Result<bool, String> {
let inline = params.get("settings").filter(|value| !value.is_null());
let named = params.get("setup").filter(|value| !value.is_null());
if inline.is_some() && named.is_some() {
return Err("choose either setup or settings".into());
}
let inline_setup = inline
.filter(|value| value.get("job").is_some_and(|job| !job.is_null()))
.map(|value| serde_json::from_value::<RenderSetup>(value.clone()).map_err(|e| e.to_string()))
.transpose()?;
let setup = if let Some(name) = named.and_then(Value::as_str) {
Some(session.doc.render.setups.get(name).ok_or("no saved render setup with that name")?)
} else {
inline_setup.as_ref()
};
if let Some(setup) = setup
&& let Some(job) = &setup.job
{
if params.as_object().is_some_and(|object| object.keys().any(|key| !matches!(key.as_str(), "setup" | "settings" | "replace"))) {
return Err("saved render jobs cannot be mixed with runtime controls".into());
}
setup.validate().map_err(|e| e.to_string())?;
return Ok(matches!(job, RenderJob::GpuPath { .. }));
}
Ok(params.get("progressive").and_then(Value::as_bool) == Some(true))
}
fn run() -> Result<(), String> {
let args: Vec<String> = std::env::args().skip(1).collect();
let [recipe_path, params_path, output_path] = args.as_slice() else {
return Err("usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png".into());
};
let output = Path::new(output_path);
if !output.extension().is_some_and(|e| e.eq_ignore_ascii_case("png")) {
return Err("output must have a .png extension".into());
}
let recipe = read_json(recipe_path)?;
let params = read_json(params_path)?;
let mut session = Session::default();
eprintln!("Generating original CAD scene from {recipe_path}");
let generation_start = Instant::now();
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
let generation_seconds = generation_start.elapsed().as_secs_f64();
let design_path = output.with_extension("solidcraft");
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
let bodies: Vec<Value> = scene
.meshes
.iter()
.map(|mesh| json!({"name":mesh.name,"vertices":mesh.mesh.positions.len(),"triangles":mesh.mesh.triangles.len()}))
.collect();
drop(scene);
let cpu_host_start = Instant::now();
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
let cpu_host_seconds = cpu_host_start.elapsed().as_secs_f64();
// Match runtime and saved-job dispatch. CPU evidence must not require a
// Vulkan device, parse GPU policy or initialize a GPU host.
let (adapter_report, gpu_host_seconds) = if needs_gpu(&session, &params)? {
let gpu_host_start = Instant::now();
let policy = solidcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
let (adapter, report) =
solidcraft_gpu::select::pick_vulkan(policy, |_| true).ok_or("no Vulkan adapter satisfies the requested GPU policy")?;
session.register_render_job_host(solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
(Some(report), Some(gpu_host_start.elapsed().as_secs_f64()))
} else {
(None, None)
};
let preflight = session.execute("render.preflight", &params).map_err(|e| e.to_string())?;
let start = Instant::now();
let started = session.execute("render.start", &params).map_err(|e| e.to_string())?;
let executor = started
.get("resolved")
.and_then(|resolved| resolved.get("executor"))
.and_then(Value::as_str)
.ok_or("started job did not report the resolved image executor")?
.to_owned();
let id: JobId = serde_json::from_value(started.get("job").cloned().ok_or("missing job id")?).map_err(|e| e.to_string())?;
let handle = session.render_job_handle(id).map_err(|e| e.to_string())?;
eprintln!("Rendering through production {executor} executor");
if let Some(adapter) = &adapter_report {
eprintln!("GPU adapter: {adapter:?}");
}
let status = loop {
match handle.wait_timeout(Duration::from_secs(10)) {
Ok(status) => break status,
Err(error) if error.code == ErrorCode::TimedOut && start.elapsed() < Duration::from_secs(3600) => {
let status = handle.status().map_err(|e| e.to_string())?;
eprintln!(
"Render progress: {:?} {}/{} after {:.1}s",
status.progress.phase,
status.progress.done,
status.progress.total,
start.elapsed().as_secs_f64()
);
}
Err(error) => {
let _ = handle.cancel();
return Err(error.to_string());
}
}
};
let render_and_encode_seconds = start.elapsed().as_secs_f64();
if status.progress.phase != Phase::Completed {
return Err(status.error.map_or_else(|| "render did not complete".into(), |e| e.to_string()));
}
let exported = session.execute("render.export", &json!({"job":id,"path":output_path,"overwrite":true})).map_err(|e| e.to_string())?;
let report = json!({
"recipe":recipe_path,"render_params":params,"executor":executor,"adapter":adapter_report,
"environment":{"SOLIDCRAFT_GPU":std::env::var("SOLIDCRAFT_GPU").ok(),"WGPU_BACKEND":std::env::var("WGPU_BACKEND").ok()},
"cad_generation_seconds":generation_seconds,"cpu_host_setup_seconds":cpu_host_seconds,"gpu_host_setup_seconds":gpu_host_seconds,
"render_and_encode_seconds":render_and_encode_seconds,
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation, preflight and initial CPU/GPU host setup; GPU setup timing includes adapter selection and pipelines when requested",
"bodies":bodies,"commands":commands,"command_timings":command_timings,
"preflight":preflight,"export":exported,"editable_design":design_path
});
std::fs::write(output.with_extension("json"), serde_json::to_vec_pretty(&report).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
println!("{}", serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?);
Ok(())
}
fn main() -> Result<(), String> {
run()
}
+2 -1
View File
@@ -36,8 +36,9 @@ impl Script {
);
}
/// A part placed after it is built: free, even as the design's first part (#2804).
fn component(&mut self, name: &str) {
self.c("component.create", json!({"name": name}));
self.c("component.create", json!({"name": name, "grounded": false}));
}
fn root(&mut self) {
+701
View File
@@ -0,0 +1,701 @@
//! `bench-assistant`: the standard Assistant model test (v1, #4851), run against a desktop app.
//!
//! The app is already running with `--control PORT`; everything goes through its control channel
//! (`docs/control-protocol.md`), so the command needs no window code of its own. For each task it
//! starts a new design, runs the task's prescribed setup (a box, for some), adds the endpoint as an
//! API agent with All permissions, starts a new chat, sends the prompt once and waits for the turn
//! to end. The result is checked on the design (`document.inspect` with measurements), never on
//! what the model says, except for the one question whose answer is the reply. A failed task is
//! put in one of the issue's failure categories from the conversation's items.
//!
//! ```text
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT --base-url URL --model NAME
//! [--token T] [--tasks 1,2,5] [--repeat N] [--timeout-s 300]
//! [--json out.json] [--markdown out.md]
//! ```
//! prints a table of the results; `--markdown` writes the issue's comment template filled in,
//! `--json` every task's rules, conversation and design before and after.
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpStream};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
/// Standard test v1, as #4851 states it. Changing a prompt or a rule makes it another test.
const TASKS: &str = r#"[
{"id": "01-cube", "prompt": "Make a 20 mm cube.", "check": {"bodies": 1, "volume": 8000, "bbox_size": [20, 20, 20]}},
{"id": "02-cylinder", "prompt": "Make a cylinder 30 mm in diameter and 50 mm tall.", "check": {"bodies": 1, "volume": 35342.92, "bbox_size": [30, 30, 50]}},
{"id": "03-block", "prompt": "Create a block 60 mm long, 40 mm wide and 10 mm thick.", "check": {"bodies": 1, "volume": 24000, "bbox_size": [10, 40, 60]}},
{"id": "04-sketch-extrude", "prompt": "Sketch a 50 by 30 mm rectangle on the XY plane and extrude it 15 mm.", "check": {"bodies": 1, "volume": 22500, "bbox_size": [15, 30, 50], "feature_kinds": ["Extrude"]}},
{"id": "05-plate-hole", "prompt": "Make a plate 80 x 60 x 10 mm with a 20 mm diameter hole straight through the middle.", "check": {"bodies": 1, "volume": 44858.41, "bbox_size": [10, 60, 80], "note": "80*60*10 - pi*10^2*10"}},
{"id": "06-fillet-box", "prompt": "Round all the edges of the box with a 3 mm radius.", "setup": [{"command": "solid.box", "params": {"length": 40, "width": 40, "height": 40}}], "check": {"bodies": 1, "volume": 63108.53, "tol": 0.003, "feature_kinds": ["Fillet"], "note": "all 12 edges of a 40 mm cube rounded r=3"}},
{"id": "07-move-body", "prompt": "Move the box 50 mm along the X axis.", "setup": [{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 20}}], "check": {"bodies": 1, "volume": 8000, "bbox_min": [50, 0, 0]}},
{"id": "08-rename-body", "prompt": "Rename the body to Bracket.", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "body_name": "Bracket"}},
{"id": "09-volume-question", "prompt": "What is the volume of the part?", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "unchanged": true, "reply_number": "6[ ,. ]?000(?![0-9])|6(\\.0+)? ?cm"}},
{"id": "10-two-holes", "prompt": "Make a 100 x 50 x 5 mm plate and drill two 10 mm holes through it, each 15 mm in from the short ends and centred across the width.", "check": {"bodies": 1, "volume": 24214.6, "bbox_size": [5, 50, 100], "note": "100*50*5 - 2*pi*5^2*5"}},
{"id": "11-any-change", "prompt": "can you use the software to make a change to the current shape, any change at all", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"changed": true, "note": "verbatim user prompt from #4846; pass = the design changed"}}
]"#;
/// The agent the test adds. A fixed id replaces it on every run instead of adding another.
const AGENT_ID: &str = "api-bench-assistant";
const AGENT_NAME: &str = "bench";
/// Longest control reply read (the channel's own request limit).
const MAX_REPLY: usize = 16 << 20;
/// Most result data kept for `--json`.
const MAX_RESULTS: usize = 64 << 20;
/// Deadline of the requests that only set up or read (each is answered at once by the app).
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
/// How often a running turn is polled.
const POLL: Duration = Duration::from_millis(250);
/// After this long, an idle chat with no reply yet counts as a turn that ended without one.
const START_GRACE: Duration = Duration::from_secs(30);
/// Absolute tolerance of the lowest corner, in millimetres.
const MIN_CORNER_TOL: f64 = 0.05;
/// A JSON-lines connection to the app. Requests are sent once; a lost reply is never retried,
/// so nothing runs twice.
struct Control {
addr: SocketAddr,
token: String,
reader: BufReader<TcpStream>,
id: u64,
}
impl Control {
fn connect(addr: &str, token: Option<&str>) -> Result<Self, String> {
let addr: SocketAddr = addr.parse().map_err(|_| format!("--connect: `{addr}` is not an IP address and port"))?;
if !addr.ip().is_loopback() || addr.port() == 0 {
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
}
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
.map(|t| t.token)
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
if token.len() > 4096 {
return Err("the control token is longer than 4096 bytes".into());
}
Self::open(addr, token)
}
fn open(addr: SocketAddr, token: String) -> Result<Self, String> {
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| format!("connecting to {addr}: {e}"))?;
Ok(Self { addr, token, reader: BufReader::new(stream), id: 0 })
}
/// A new connection to the same app: a request that outlived its deadline keeps the old one
/// busy until it is answered.
fn reopen(&self) -> Result<Self, String> {
Self::open(self.addr, self.token.clone())
}
fn call(&mut self, method: &str, params: Value, deadline: Instant) -> Result<Value, String> {
let left = || deadline.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()).ok_or_else(|| "timeout".to_string());
let io = |e: std::io::Error| match e.kind() {
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timeout".to_string(),
_ => format!("control channel: {e}"),
};
self.reader.get_ref().set_write_timeout(Some(left()?)).map_err(io)?;
self.id += 1;
// The first request on a connection carries the token; sending it on every one is allowed.
let request = json!({"id": self.id, "method": method, "params": params, "token": self.token});
writeln!(self.reader.get_mut(), "{request}").map_err(io)?;
let mut line = Vec::new();
loop {
self.reader.get_ref().set_read_timeout(Some(left()?)).map_err(io)?;
let bytes = self.reader.fill_buf().map_err(io)?;
if bytes.is_empty() {
return Err("the app closed the control connection".into());
}
let newline = bytes.iter().position(|b| *b == b'\n');
let used = newline.map_or(bytes.len(), |n| n + 1);
if line.len() + used > MAX_REPLY {
return Err("a control reply is larger than 16 MiB".into());
}
line.extend_from_slice(bytes.get(..used).unwrap_or_default());
self.reader.consume(used);
if newline.is_some() {
break;
}
}
let reply: Value = serde_json::from_slice(&line).map_err(|e| format!("control reply: {e}"))?;
if reply["id"].as_u64() != Some(self.id) {
return Err("control reply for another request".into());
}
if reply["ok"] != true {
return Err(format!("{method}: {}", reply["error"].as_str().unwrap_or("failed")));
}
reply.get("result").cloned().ok_or_else(|| format!("{method}: reply without a result"))
}
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String> {
self.call("engine.execute", json!({"command": command, "params": params, "animate": false}), Instant::now() + REQUEST_TIMEOUT)
}
fn chat(&mut self, params: Value) -> Result<Value, String> {
self.call("ui.agent_chat", params, Instant::now() + REQUEST_TIMEOUT)
}
fn inspect(&mut self) -> Result<Value, String> {
let design = self.call("document.inspect", json!({"measure": true}), Instant::now() + REQUEST_TIMEOUT)?;
if design.is_object() { Ok(design) } else { Err("document.inspect returned no design".into()) }
}
}
fn tasks() -> Result<Vec<Value>, String> {
serde_json::from_str(TASKS).map_err(|e| format!("embedded tasks: {e}"))
}
fn triple(value: &Value) -> Option<[f64; 3]> {
match value.as_array()?.as_slice() {
[x, y, z] => {
let n = |v: &Value| v.as_f64().filter(|n| n.is_finite());
Some([n(x)?, n(y)?, n(z)?])
}
_ => None,
}
}
/// What the model said (thoughts left out).
fn reply(items: &[Value]) -> String {
items.iter().filter(|i| i["thought"] != true).filter_map(|i| i["agent"].as_str()).collect::<Vec<_>>().join("\n")
}
/// v1's reply pattern `6[ ,. ]?000(?![0-9])|6(\.0+)? ?cm`: "6000", "6 000", "6,000", "6.000" not
/// followed by a digit, or "6 cm", "6.0cm", … (a narrow no-break space also separates thousands).
fn volume_reply(text: &str) -> bool {
text.match_indices('6').any(|(at, _)| {
let tail = text.get(at + 1..).unwrap_or_default();
let thousands = |t: &str| t.strip_prefix("000").is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_digit()));
let separated = tail.strip_prefix([' ', ',', '.', '\u{202f}']).is_some_and(thousands);
let cm = {
let t = tail.strip_prefix(".0").map_or(tail, |t| t.trim_start_matches('0'));
t.strip_prefix(' ').unwrap_or(t).starts_with("cm")
};
thousands(tail) || separated || cm
})
}
/// The design as the user sees it, without what reading or saving changes (path, saved state,
/// revision, selection, undo depth, evaluation timings).
fn content(design: &Value) -> Value {
let mut v = design.clone();
if let Some(o) = v.as_object_mut() {
for key in ["path", "dirty", "revision", "selection", "undo", "redo"] {
o.remove(key);
}
}
for feature in v.get_mut("timeline").and_then(Value::as_array_mut).into_iter().flatten() {
if let Some(o) = feature.as_object_mut() {
o.remove("ms");
o.remove("boolean_diagnostics");
}
}
v
}
/// Whether the design changed: its content, or a step on the undo stack (a colour, say, that the
/// inspection does not list).
fn changed(before: &Value, after: &Value) -> bool {
content(before) != content(after) || after["undo"].as_u64() > before["undo"].as_u64()
}
/// The rules a task's result breaks (none: it passed). `before` and `after` are
/// `document.inspect {measure: true}` before the prompt and after the turn.
pub(super) fn check(rule: &Value, before: &Value, after: &Value, items: &[Value]) -> Vec<String> {
let Some(all) = after["bodies"].as_array() else { return vec!["the design could not be read".into()] };
// Bodies the user sees; a hidden one (a tool body, a hidden original) is not the result.
let bodies: Vec<&Value> = all.iter().filter(|b| b["visible"] != false).collect();
let mut errors = Vec::new();
if let Some(n) = rule["bodies"].as_u64()
&& bodies.len() as u64 != n
{
errors.push(format!("{} bodies, expected {n}", bodies.len()));
}
if let Some(expected) = rule["volume"].as_f64() {
let tol = rule["tol"].as_f64().unwrap_or(0.01);
let volumes: Vec<f64> = bodies.iter().filter_map(|b| b["volume_mm3"].as_f64()).filter(|v| v.is_finite()).collect();
let total: f64 = volumes.iter().sum();
if volumes.len() != bodies.len() || (total - expected).abs() > expected * tol {
errors.push(format!("volume {total:.1} mm³, expected {expected} mm³ ±{}%", tol * 100.0));
}
}
let first = bodies.first().copied().unwrap_or(&Value::Null);
let min = triple(&first["bbox"]["min"]);
if let Some(mut expected) = triple(&rule["bbox_size"]) {
expected.sort_by(f64::total_cmp);
let size = min.zip(triple(&first["bbox"]["max"])).map(|(lo, hi)| {
let mut size = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
size.sort_by(f64::total_cmp);
size
});
if !size.is_some_and(|s| s.iter().zip(expected).all(|(a, e)| (a - e).abs() <= 0.01 * e.max(1.0))) {
errors.push(format!("size {size:?}, expected {expected:?} in any orientation"));
}
}
if let Some(expected) = triple(&rule["bbox_min"])
&& !min.is_some_and(|m| m.iter().zip(expected).all(|(a, e)| (a - e).abs() <= MIN_CORNER_TOL))
{
errors.push(format!("lowest corner {min:?}, expected {expected:?}"));
}
if let Some(name) = rule["body_name"].as_str()
&& !bodies.iter().any(|b| b["name"] == name)
{
errors.push(format!("no body named {name}"));
}
for kind in rule["feature_kinds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
let live = |f: &&Value| f["suppressed"] != true && f["rolled_back"] != true && f["error"].is_null();
if !after["timeline"].as_array().into_iter().flatten().filter(live).any(|f| f["type"].as_str().is_some_and(|t| t.contains(kind))) {
errors.push(format!("no {kind} in the timeline"));
}
}
if rule["changed"] == true || rule["unchanged"] == true {
let changed = before.is_object() && changed(before, after);
if rule["changed"] == true && !changed {
errors.push("the design did not change".into());
}
if rule["unchanged"] == true && (changed || !before.is_object()) {
errors.push("the design changed".into());
}
}
if rule["reply_number"].is_string() && !volume_reply(&reply(items)) {
errors.push("the reply does not give the volume".into());
}
errors
}
/// A tool call written into the reply as JSON: `{"name": …`, `{"command": …`, `{"function": …`,
/// `{"tool": …`.
fn call_as_text(text: &str) -> bool {
text.match_indices('{').any(|(at, _)| {
let rest = text.get(at + 1..).unwrap_or_default().trim_start();
["\"name\"", "\"command\"", "\"function\"", "\"tool\""]
.iter()
.any(|key| rest.strip_prefix(key).is_some_and(|r| r.trim_start().starts_with(':')))
})
}
/// How a failed task failed, in the issue's words, from the conversation's items.
pub(super) fn classify(items: &[Value], timed_out: bool) -> &'static str {
if timed_out {
return "timeout";
}
let tools: Vec<&Value> = items.iter().filter(|i| i.get("tool").is_some()).collect();
// A failed call, or a batch whose answer reports a failed step (`"ok": false`).
let failed: Vec<String> = tools
.iter()
.map(|t| t["output"].as_str().unwrap_or_default())
.zip(&tools)
.filter(|(output, t)| t["status"] == "Failed" || serde_json::from_str::<Value>(output).is_ok_and(|v| v["ok"] == false))
.map(|(output, _)| output.to_lowercase())
.collect();
let notices: Vec<String> = items.iter().filter_map(|i| i["notice"].as_str()).map(str::to_lowercase).collect();
let answer = reply(items);
if notices.iter().any(|n| n.contains("http 400")) {
return "the endpoint refused tools (HTTP 400)";
}
if notices.iter().any(|n| n.contains("max_turn_requests")) || answer.contains("requests for one message") {
return "hit the request limit";
}
if call_as_text(&answer) {
return "wrote the call as text";
}
if failed.iter().any(|o| o.contains("unknown command") || o.starts_with("unknown tool")) {
return "wrong or unknown command";
}
if failed.iter().any(|o| o.contains("invalid parameters") || o.starts_with("not run:") || o.contains("not valid json")) {
return "wrong arguments";
}
if !failed.is_empty() || notices.iter().any(|n| n.starts_with("http ")) || items.iter().any(|i| i["error"] == true) {
return "gave up after errors";
}
if tools.is_empty() {
return if answer.trim().is_empty() { "an empty reply" } else { "only talked" };
}
"wrong result"
}
/// One run's results in the issue's comment template (`run`: which repetition).
pub(super) fn markdown(model: &str, version: &str, results: &[Value], run: u64) -> String {
let clean = |s: &str| s.replace(['\n', '\r', '|'], " ");
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
let passed = rows.iter().filter(|r| r["passed"] == true).count();
let mut out = format!(
"### {} (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: {}\nResult: {passed}/11\n| # | pass/fail | how it failed |\n|---|---|---|\n",
clean(model),
clean(version)
);
for n in 1..=11 {
match rows.iter().find(|r| r["task"] == n) {
Some(r) if r["passed"] == true => out.push_str(&format!("| {n} | pass | |\n")),
Some(r) => out.push_str(&format!("| {n} | fail | {} |\n", clean(r["failure"].as_str().unwrap_or_default()))),
None => out.push_str(&format!("| {n} | | not run |\n")),
}
}
out.push_str(if rows.len() == 11 {
"Notes: <anything odd, e.g. very slow, needed a retry>\n"
} else {
"Notes: partial run; the tasks marked \"not run\" were left out.\n"
});
out
}
fn write(path: &str, text: &str) -> Result<(), String> {
solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"))
}
struct Options<'a> {
connect: &'a str,
token: Option<&'a str>,
base_url: &'a str,
model: &'a str,
tasks: Vec<usize>,
repeat: u64,
timeout: Duration,
json: Option<&'a str>,
markdown: Option<&'a str>,
}
fn options(args: &[String]) -> Result<Options<'_>, String> {
const FLAGS: [&str; 9] = ["--connect", "--token", "--base-url", "--model", "--tasks", "--repeat", "--timeout-s", "--json", "--markdown"];
let mut given = std::collections::BTreeMap::new();
let mut args = args.iter();
while let Some(flag) = args.next() {
let Some(flag) = FLAGS.iter().find(|f| **f == flag.as_str()) else { return Err(format!("unknown bench-assistant option: {flag}")) };
let value = args
.next()
.filter(|v| !v.starts_with("--") && !v.is_empty() && v.len() <= 4096)
.ok_or_else(|| format!("{flag} needs a value (at most 4096 bytes)"))?;
if given.insert(*flag, value.as_str()).is_some() {
return Err(format!("{flag} given twice"));
}
}
let required = |key: &str| given.get(key).copied().ok_or_else(|| format!("bench-assistant needs {key}"));
let number = |key: &str, default: u64, max: u64| -> Result<u64, String> {
given
.get(key)
.map_or(Ok(default), |s| s.parse::<u64>().ok().filter(|n| (1..=max).contains(n)).ok_or_else(|| format!("{key} must be 1 to {max}")))
};
let tasks = match given.get("--tasks") {
None => (1..=11).collect(),
Some(list) => {
let mut tasks = Vec::new();
for n in list.split(',') {
let n = n
.trim()
.parse::<usize>()
.ok()
.filter(|n| (1..=11).contains(n))
.ok_or("--tasks takes task numbers 1 to 11, separated by commas")?;
if tasks.contains(&n) {
return Err(format!("--tasks lists task {n} twice"));
}
tasks.push(n);
}
tasks
}
};
Ok(Options {
connect: required("--connect")?,
token: given.get("--token").copied(),
base_url: required("--base-url")?,
model: required("--model")?,
tasks,
repeat: number("--repeat", 1, 100)?,
timeout: Duration::from_secs(number("--timeout-s", 300, 3600)?),
json: given.get("--json").copied(),
markdown: given.get("--markdown").copied(),
})
}
/// Waits for the turn the prompt started to end. Returns the chat's last state and whether the
/// deadline passed first.
fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: Instant) -> Result<(Value, bool), String> {
let mut seen_busy = false;
loop {
let state = chat["state"].as_str().unwrap_or_default();
let turn = !chat["turn"].is_null();
let queued = chat["pending"].as_array().is_some_and(|p| !p.is_empty());
seen_busy |= state == "busy" || turn;
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
// A turn can start and end between two polls: then the conversation has its answer.
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
return Ok((chat, false));
}
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
std::thread::sleep(POLL.min(left));
match control.call("ui.agent_chat", json!({}), end) {
Ok(next) => chat = next,
Err(e) if e == "timeout" => return Ok((chat, true)),
Err(e) => return Err(e),
}
}
}
pub fn run(args: &[String]) -> Result<(), String> {
let o = options(args)?;
let tasks = tasks()?;
let mut control = Control::connect(o.connect, o.token)?;
let help = control.call("ui.help", json!({}), Instant::now() + REQUEST_TIMEOUT)?;
let version = help["version"].as_str().filter(|s| !s.is_empty() && s.len() <= 256).ok_or("the app reported no version")?.to_string();
let config = control.chat(json!({
"features": true,
"add_agent": {"id": AGENT_ID, "name": AGENT_NAME, "kind": "api", "base_url": o.base_url, "model": o.model},
}))?;
if !config["agents"].as_array().into_iter().flatten().any(|a| a["id"] == AGENT_ID) {
return Err("the app did not add the benchmark's agent".into());
}
// Put the user's permission setting back afterwards.
let permissions = config["permissions"].as_str().map(str::to_string);
let result = run_tasks(&mut control, &o, &tasks, &version);
let _ = control.chat(json!({"stop": true}));
if let Some(p) = permissions {
let _ = control.chat(json!({"permissions": p}));
}
result
}
fn run_tasks(control: &mut Control, o: &Options, tasks: &[Value], version: &str) -> Result<(), String> {
let mut results: Vec<Value> = Vec::new();
let mut kept = 0;
let mut stdout = std::io::stdout().lock();
let mut say = |line: String| writeln!(stdout, "{line}").map_err(|e| format!("stdout: {e}"));
say(format!("bench-assistant v1: {} on SolidCraft {version}", o.model))?;
say("| run | # | task | pass/fail | how it failed | seconds |\n|---|---|---|---|---|---|".into())?;
for run in 1..=o.repeat {
for &n in &o.tasks {
let task = tasks.get(n - 1).ok_or("missing embedded task")?;
// A fresh design and a fresh chat, with nothing of the last task still running.
control.chat(json!({"stop": true}))?;
control.execute("file.new", json!({"name": "Benchmark"}))?;
for step in task["setup"].as_array().into_iter().flatten() {
control.execute(step["command"].as_str().ok_or("embedded setup without a command")?, step["params"].clone())?;
}
control.chat(json!({"agent": AGENT_ID, "permissions": "all", "new_chat": true, "attach_view": false}))?;
let before = control.inspect()?;
let began = Instant::now();
let end = began + o.timeout;
let (chat, timed_out) = match control.call("ui.agent_chat", json!({"send": task["prompt"]}), end) {
Ok(chat) => wait_for_turn(control, chat, began, end)?,
Err(e) if e == "timeout" => (json!({}), true),
Err(e) => return Err(e),
};
if timed_out {
// The late reply would arrive on the old connection; stop the model on a new one
// before reading the design.
*control = control.reopen()?;
}
// Stop the agent before reading the result, so nothing changes the design meanwhile.
let stopped = control.chat(json!({"stop": true}))?;
let items = stopped["items"].as_array().or(chat["items"].as_array()).cloned().unwrap_or_default();
let after = control.inspect()?;
let failed_rules = check(&task["check"], &before, &after, &items);
let passed = failed_rules.is_empty();
let failure = if passed { "" } else { classify(&items, timed_out) };
let seconds = began.elapsed().as_secs_f64();
say(format!(
"| {run} | {n} | {} | {} | {failure} | {seconds:.0} |",
task["id"].as_str().unwrap_or_default(),
if passed { "pass" } else { "fail" }
))?;
let result = json!({
"task": n, "id": task["id"], "prompt": task["prompt"], "repeat": run, "passed": passed, "failure": failure,
"failed_rules": failed_rules, "timed_out": timed_out, "seconds": seconds, "check": task["check"],
"items": items, "before": before, "after": after,
});
kept += result.to_string().len();
if kept > MAX_RESULTS {
return Err("the results passed 64 MiB; run fewer tasks or repetitions".into());
}
results.push(result);
// Written after every task, so an interrupted run keeps what it measured.
if let Some(path) = o.json {
let all = json!({"test": "v1", "model": o.model, "base_url": o.base_url, "solidcraft": version, "results": results});
write(path, &serde_json::to_string_pretty(&all).map_err(|e| e.to_string())?)?;
}
if let Some(path) = o.markdown {
write(path, &(1..=run).map(|r| markdown(o.model, version, &results, r)).collect::<Vec<_>>().join("\n"))?;
}
}
}
for run in 1..=o.repeat {
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
say(format!("run {run}: {}/{} passed", rows.iter().filter(|r| r["passed"] == true).count(), rows.len()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn design(bodies: Value, timeline: Value) -> Value {
json!({"name": "Benchmark", "params": [], "timeline": timeline, "bodies": bodies, "sketches": [], "undo": 1, "revision": 3, "dirty": true})
}
fn body(name: &str, volume: f64, min: [f64; 3], max: [f64; 3]) -> Value {
json!({"name": name, "volume_mm3": volume, "bbox": {"min": min, "max": max}, "visible": true})
}
#[test]
fn the_embedded_tasks_are_v1() {
let tasks = tasks().unwrap();
assert_eq!(tasks.len(), 11);
assert_eq!(tasks[0]["prompt"], "Make a 20 mm cube.");
assert_eq!(tasks[10]["prompt"], "can you use the software to make a change to the current shape, any change at all");
assert_eq!(tasks[5]["check"]["tol"], 0.003);
assert_eq!(tasks[6]["check"]["bbox_min"], json!([50, 0, 0]));
let boxes: Vec<_> = tasks.iter().filter(|t| t["setup"].is_array()).map(|t| t["id"].as_str().unwrap()).collect();
assert_eq!(boxes, ["06-fillet-box", "07-move-body", "08-rename-body", "09-volume-question", "11-any-change"]);
}
#[test]
fn pass_rules() {
let tasks = tasks().unwrap();
let rule = |n: usize| tasks[n - 1]["check"].clone();
let empty = design(json!([]), json!([]));
let pass = |n: usize, before: &Value, after: &Value, items: &[Value]| check(&rule(n), before, after, items);
// 3: any orientation, within 1%.
let block = design(json!([body("Body1", 24_100.0, [0.0; 3], [40.0, 10.0, 60.2])]), json!([{"type": "Box"}]));
assert!(pass(3, &empty, &block, &[]).is_empty());
let thick = design(json!([body("Body1", 24_000.0, [0.0; 3], [40.0, 12.0, 60.0])]), json!([]));
assert_eq!(pass(3, &empty, &thick, &[]).len(), 1);
let two = design(json!([body("A", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0]), body("B", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0])]), json!([]));
assert!(!pass(3, &empty, &two, &[]).is_empty());
// A hidden body is not part of the result.
let mut hidden = two.clone();
hidden["bodies"][1]["visible"] = json!(false);
hidden["bodies"][0]["volume_mm3"] = json!(24_000.0);
hidden["bodies"][0]["bbox"]["max"] = json!([60.0, 40.0, 10.0]);
assert!(pass(3, &empty, &hidden, &[]).is_empty());
// 4: the timeline needs a working Extrude.
let plate = |kind: Value| design(json!([body("Body1", 22_500.0, [0.0; 3], [50.0, 30.0, 15.0])]), kind);
assert!(pass(4, &empty, &plate(json!([{"type": "Sketch"}, {"type": "Extrude"}])), &[]).is_empty());
assert!(!pass(4, &empty, &plate(json!([{"type": "Box"}])), &[]).is_empty());
for flag in [json!({"suppressed": true}), json!({"rolled_back": true}), json!({"error": "failed"})] {
let mut feature = json!({"type": "Extrude"});
feature.as_object_mut().unwrap().extend(flag.as_object().unwrap().clone());
assert!(!pass(4, &empty, &plate(json!([feature])), &[]).is_empty(), "{flag}");
}
// 6: the fillet's tolerance is 0.3%.
let start = design(json!([body("Body1", 64_000.0, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}]));
let filleted = |v: f64| design(json!([body("Body1", v, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}, {"type": "Fillet"}]));
assert!(pass(6, &start, &filleted(63_108.53 * 1.002), &[]).is_empty());
assert!(!pass(6, &start, &filleted(63_108.53 * 1.004), &[]).is_empty());
// 7: the lowest corner.
let moved = |x: f64| design(json!([body("Body1", 8000.0, [x, 0.0, 0.0], [x + 20.0, 20.0, 20.0])]), json!([]));
assert!(pass(7, &empty, &moved(50.0), &[]).is_empty());
assert!(!pass(7, &empty, &moved(0.0), &[]).is_empty());
// 8: the name.
let named = |name: &str| design(json!([body(name, 6000.0, [0.0; 3], [30.0, 20.0, 10.0])]), json!([]));
assert!(pass(8, &empty, &named("Bracket"), &[]).is_empty());
assert!(!pass(8, &empty, &named("Body1"), &[]).is_empty());
// 9: unchanged, and the reply gives the volume.
let start = named("Body1");
let said = |text: &str| [json!({"user": "What is the volume of the part?"}), json!({"agent": text, "thought": false})];
let mut read = start.clone();
read["revision"] = json!(7);
read["dirty"] = json!(false);
read["selection"] = json!([{"body": "Body1"}]);
assert!(pass(9, &start, &read, &said("The volume is 6000 mm³.")).is_empty());
assert!(!pass(9, &start, &read, &said("About 60 cm³.")).is_empty());
assert!(!pass(9, &start, &named("Bracket"), &said("6 000 mm³")).is_empty());
let mut undone = start.clone();
undone["undo"] = json!(2);
assert!(!pass(9, &start, &undone, &said("6000 mm³")).is_empty());
// 11: any change counts; reading does not.
assert!(pass(11, &start, &named("Bracket"), &[]).is_empty());
assert!(pass(11, &start, &undone, &[]).is_empty());
assert!(!pass(11, &start, &read, &[]).is_empty());
// An unreadable design passes nothing.
assert!(!pass(11, &start, &json!({}), &[]).is_empty());
}
#[test]
fn the_volume_reply() {
for yes in ["6000", "6 000 mm³", "6,000", "6.000", "6\u{202f}000", "6 cm³", "6.00cm3", "is 6000."] {
assert!(volume_reply(yes), "{yes}");
}
for no in ["60000", "6,0001", "60 cm³", "600", "6.01 cm³", ""] {
assert!(!volume_reply(no), "{no}");
}
}
#[test]
fn failure_categories() {
for (items, expected) in [
(
json!([{"user": "Make a 20 mm cube."}, {"agent": "{\"name\": \"execute\", \"arguments\": {}}", "thought": false}]),
"wrote the call as text",
),
(
json!([{"notice": "HTTP 400: registry.ollama.ai/library/gemma3:4b does not support tools", "error": true}]),
"the endpoint refused tools (HTTP 400)",
),
(json!([{"user": "Make a 20 mm cube."}, {"agent": "", "thought": false}]), "an empty reply"),
(json!([{"agent": "Let me think", "thought": true}]), "an empty reply"),
(json!([{"agent": "Sure, here is how to make a cube.", "thought": false}]), "only talked"),
(
json!([{"tool": "1", "status": "Failed", "output": "unknown command `solid.cube` (did you mean `solid.box`?)"}]),
"wrong or unknown command",
),
(json!([{"tool": "1", "status": "Failed", "output": "unknown tool `make_cube`"}]), "wrong or unknown command"),
(json!([{"tool": "1", "status": "Failed", "output": "invalid parameters for `solid.box`: `length` is required"}]), "wrong arguments"),
(json!([{"tool": "1", "status": "Failed", "output": "Not run: the tool arguments were not valid JSON"}]), "wrong arguments"),
(json!([{"tool": "1", "status": "Failed", "output": "no body `Box`"}]), "gave up after errors"),
(
json!([{"tool": "1", "status": "Completed", "output": "{\"ok\": false, \"failed\": {\"error\": \"unknown command `solid.delete`\"}}"}]),
"wrong or unknown command",
),
(
json!([{"tool": "1", "status": "Completed"}, {"agent": "\n(stopped after 24 requests for one message; send another message to go on)"}]),
"hit the request limit",
),
(json!([{"tool": "1", "status": "Completed", "output": "{}"}]), "wrong result"),
] {
let items = items.as_array().unwrap();
assert_eq!(classify(items, false), expected, "{items:?}");
assert_eq!(classify(items, true), "timeout");
}
}
#[test]
fn markdown_is_the_comment_template() {
let rows: Vec<Value> =
(1..=11).map(|n| json!({"task": n, "repeat": 1, "passed": n != 2, "failure": if n == 2 { "an empty reply" } else { "" }})).collect();
let report = markdown("qwen2.5:7b", "0.19.0", &rows, 1);
assert!(report.starts_with(
"### qwen2.5:7b (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: 0.19.0\nResult: 10/11\n| # | pass/fail | how it failed |\n|---|---|---|\n| 1 | pass | |\n| 2 | fail | an empty reply |\n"
));
assert!(report.ends_with("| 11 | pass | |\nNotes: <anything odd, e.g. very slow, needed a retry>\n"));
let partial = markdown("m|x", "v", &rows[..1], 1);
assert!(partial.starts_with("### m x ("));
assert!(partial.contains("Result: 1/11") && partial.contains("| 2 | | not run |") && partial.contains("partial run"));
}
#[test]
fn options_are_checked_before_connecting() {
for extra in [
&["--repeat", "0"][..],
&["--repeat", "101"],
&["--timeout-s", "3601"],
&["--tasks", "0"],
&["--tasks", "12"],
&["--tasks", "1,1"],
&["--model", "again"],
&["--unexpected", "1"],
&["--token"],
] {
let mut args: Vec<String> =
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "http://127.0.0.1:11434/v1"].iter().map(|s| s.to_string()).collect();
args.extend(extra.iter().map(|s| s.to_string()));
assert!(options(&args).is_err(), "{extra:?}");
}
let args: Vec<String> =
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "u", "--tasks", "2, 5"].iter().map(|s| s.to_string()).collect();
let o = options(&args).unwrap();
assert_eq!((o.tasks, o.repeat, o.timeout), (vec![2, 5], 1, Duration::from_secs(300)));
assert!(Control::connect("10.0.0.1:9271", Some("t")).is_err());
assert!(Control::connect("localhost", Some("t")).is_err());
}
}
+6 -3
View File
@@ -6,11 +6,14 @@ use std::sync::Mutex;
use serde_json::{Value, json};
use solidcraft_engine::crash;
const USAGE: &str = "usage: solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]";
/// The command's usage, from the same lines the top-level usage lists.
fn usage() -> String {
crate::command_usage("crashes")
}
pub fn run(args: &[String]) -> Result<(), String> {
let dir = crash::default_dir().ok_or("no crash folder on this system")?;
let id = || args.get(1).map(String::as_str).ok_or_else(|| USAGE.to_string());
let id = || args.get(1).map(String::as_str).ok_or_else(usage);
match args.first().map(String::as_str) {
None | Some("list") => {
let reports = crash::list(&dir);
@@ -43,7 +46,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
println!("deleted {} report(s)", crash::clear(&dir, args.get(1).map(String::as_str)));
Ok(())
}
_ => Err(USAGE.to_string()),
_ => Err(usage()),
}
}
+10
View File
@@ -16,9 +16,16 @@ fn flag<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).and_then(|v| v.parse().ok()).unwrap_or(default)
}
/// The options `fuzz` takes, and the ones that stand alone.
const OPTIONS: &[&str] = &["--from", "--count", "--jobs", "--timeout", "--steps", "--out", "--report"];
const SWITCHES: &[&str] = &["--minimise"];
/// One seed: a design and a boolean case, as JSON on stdout (progress on stderr).
pub fn one(args: &[String]) -> Result<(), String> {
let seed: u64 = args.first().and_then(|s| s.parse().ok()).ok_or("fuzz-one <seed> [--steps N] [--minimise]")?;
// The seed is the first argument; the rest are options, and an option `fuzz-one` doesn't
// take is refused rather than ignored.
crate::known_args("fuzz-one", &args[1..], &["--steps"], SWITCHES, false)?;
let steps = flag(args, "--steps", 8usize);
let minimise = args.iter().any(|a| a == "--minimise");
// Panics caught inside the kernel: harmless natively, fatal in the web build.
@@ -64,6 +71,9 @@ struct Job {
/// Run seeds `--from`…`--from + --count` in `--jobs` processes, `--timeout` seconds each;
/// write failing scripts to `--out` and a summary to `--report`.
pub fn run(args: &[String]) -> Result<(), String> {
// An option `fuzz` doesn't take is refused before a seed runs: it would otherwise be
// ignored, and the run would go on for minutes with the wrong settings.
crate::known_args("fuzz", args, OPTIONS, SWITCHES, false)?;
let from = flag(args, "--from", 0u64);
let count = flag(args, "--count", 100u64);
let last = from.checked_add(count.saturating_sub(1)).ok_or("seed range exceeds u64::MAX: reduce --from or --count")?;
+49
View File
@@ -0,0 +1,49 @@
//! What the engine logs (`log::info!`, `log::debug!`: why a body has no display plan, why a save
//! left its model cache out), written to stderr. Off unless asked for, so the CLI's output and
//! the scripts reading it stay as they are: `--debug` shows SolidCraft's own debug messages (and
//! other crates' warnings, whose debug output is large), `RUST_LOG=error|warn|info|debug|trace`
//! sets one level for every crate.
use log::{LevelFilter, Log, Metadata, Record};
struct Stderr {
/// The level for SolidCraft's crates.
ours: LevelFilter,
/// The level for every other crate.
others: LevelFilter,
}
impl Log for Stderr {
fn enabled(&self, m: &Metadata) -> bool {
m.level() <= if m.target().starts_with("solidcraft") { self.ours } else { self.others }
}
fn log(&self, r: &Record) {
if self.enabled(r.metadata()) {
use std::io::Write as _;
// A closed stderr is no reason to stop.
let _ = writeln!(std::io::stderr().lock(), "[{} {}] {}", r.level().as_str().to_ascii_lowercase(), r.target(), r.args());
}
}
fn flush(&self) {}
}
/// Removes `--debug` from `args`, wherever it is (every subcommand takes it), and installs the
/// logger when it or `RUST_LOG` asks for one.
pub fn init(args: &mut Vec<String>) {
let before = args.len();
args.retain(|a| a != "--debug");
let debug = args.len() != before;
let env = std::env::var("RUST_LOG").ok().and_then(|v| v.trim().parse::<LevelFilter>().ok());
let (ours, others) = match (debug, env) {
(false, Some(all)) => (all, all),
(true, Some(all)) => (all.max(LevelFilter::Debug), all),
(true, None) => (LevelFilter::Debug, LevelFilter::Warn),
(false, None) => return,
};
// Installed once, for the life of the process.
if log::set_logger(Box::leak(Box::new(Stderr { ours, others }))).is_ok() {
log::set_max_level(ours.max(others));
}
}
+206 -64
View File
@@ -6,24 +6,32 @@
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
//! the standard Assistant model test (#4851) on a running app
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
//!
//! --debug (with any command) SolidCraft's debug log on stderr (or RUST_LOG=LEVEL)
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod bench_agent;
mod bench_assistant;
mod crashes;
mod fuzz;
mod logger;
mod oracle;
mod preview;
mod recipe;
@@ -39,54 +47,98 @@ mod app_icon;
use solidcraft_engine::Session;
use solidcraft_engine::view::Guides;
const USAGE: &str = "usage:
solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
(--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;
--profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)
solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)
(--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,
as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
(the design's history as a command script that eval and run replay)
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]
(--ops: after opening, run each command of the script and undo it, timing the command, the
evaluations it caused and the view it leaves: placing and meshing what changed)
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]
[--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...
(as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;
--view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;
--camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);
--no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;
--body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside
it shows) and color #rrggbb, either or both, once per body;
--tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body
is visible inside the material it meets; a later --body overrides it;
--section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world
coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)
solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]
solidcraft-cli exec <command> [json-params]
solidcraft-cli commands
solidcraft-cli recipe <recipe.json>
solidcraft-cli oracle <case-dir>... [--json]
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
[--param-check off|warn|strict]
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
solidcraft-cli licences
solidcraft-cli gpu [--policy auto|hardware|software|cpu]
(the GPU adapters a headless render can open, best first, the one the policy picks and the tier it
lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults
to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
";
/// One command the CLI takes: the name to type, the usage it prints for `--help`,
/// and whether the top-level usage lists it (an alias answers to another command's name).
struct Command {
name: &'static str,
usage: &'static str,
listed: bool,
}
/// `run`'s lines in the usage.
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
/// `eval`'s lines in the usage.
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
/// `script`'s lines in the usage.
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
/// `bench-agent`'s lines in the usage.
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
/// `bench-assistant`'s lines in the usage.
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
/// `snapshot`'s lines in the usage.
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
/// `render`'s lines in the usage.
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
/// `exec`'s lines in the usage.
const USAGE_EXEC: &str = " solidcraft-cli exec <command> [json-params]\n";
/// `commands`'s lines in the usage.
const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
/// `recipe`'s lines in the usage.
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
/// `oracle`'s lines in the usage.
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
/// `fuzz`'s lines in the usage.
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
/// `mcp`'s lines in the usage.
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
/// `crashes`'s lines in the usage.
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
/// `licences`'s lines in the usage.
const USAGE_LICENCES: &str = " solidcraft-cli licences\n";
/// `gpu`'s lines in the usage.
const USAGE_GPU: &str = " solidcraft-cli gpu [--policy auto|hardware|software|cpu]\n (the GPU adapters a headless render can open, best first, the one the policy picks and the tier it\n lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults\n to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)\n";
/// `icon`'s lines in the usage.
const USAGE_ICON: &str = " solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico\n";
/// `fuzz-one`'s lines in the usage: a seed and the flags of one fuzz run.
const USAGE_FUZZ_ONE: &str = " solidcraft-cli fuzz-one <seed> [--steps N] [--minimise]\n";
/// `bench-edit`'s lines in the usage: the parameter a script changes.
const USAGE_BENCH_EDIT: &str = " solidcraft-cli bench-edit <script.json|recipe.json> <param> <expr>\n";
/// `save-stress`'s lines in the usage: the file saved over and over.
const USAGE_SAVE_STRESS: &str = " solidcraft-cli save-stress <path> <rounds>\n";
/// The flags every command takes, at the end of the top-level usage.
const USAGE_DEBUG: &str = " --debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its\n model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate\n";
/// The commands the CLI takes, in the order the top-level usage lists them.
const COMMANDS: &[Command] = &[
Command { name: "run", usage: USAGE_RUN, listed: true },
Command { name: "eval", usage: USAGE_EVAL, listed: true },
Command { name: "inspect", usage: USAGE_EVAL, listed: false },
Command { name: "script", usage: USAGE_SCRIPT, listed: true },
Command { name: "bench-open", usage: USAGE_BENCH_OPEN, listed: true },
Command { name: "bench-agent", usage: USAGE_BENCH_AGENT, listed: true },
Command { name: "bench-assistant", usage: USAGE_BENCH_ASSISTANT, listed: true },
Command { name: "snapshot", usage: USAGE_SNAPSHOT, listed: true },
Command { name: "render", usage: USAGE_RENDER, listed: true },
Command { name: "exec", usage: USAGE_EXEC, listed: true },
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
Command { name: "mcp", usage: USAGE_MCP, listed: true },
Command { name: "crashes", usage: USAGE_CRASHES, listed: true },
Command { name: "licences", usage: USAGE_LICENCES, listed: true },
Command { name: "licenses", usage: USAGE_LICENCES, listed: false },
Command { name: "gpu", usage: USAGE_GPU, listed: true },
Command { name: "icon", usage: USAGE_ICON, listed: true },
Command { name: "bench-edit", usage: USAGE_BENCH_EDIT, listed: false },
Command { name: "save-stress", usage: USAGE_SAVE_STRESS, listed: false },
];
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut args: Vec<String> = std::env::args().skip(1).collect();
logger::init(&mut args);
let r = match args.first().map(String::as_str) {
// Asked-for help is not an error: the command's usage on stdout, exit 0. It is read
// before the command runs, so `--help` never starts a run (a fuzz run, a server).
Some(cmd) if wants_help(&args[1..]) && is_command(cmd) => {
use std::io::Write as _;
let _ = std::io::stdout().lock().write_all(command_usage(cmd).as_bytes());
Ok(())
}
Some("run") => cmd_run(&args[1..]),
Some("eval" | "inspect") => cmd_eval(&args[1..]),
Some("script") => cmd_script(&args[1..]),
@@ -107,7 +159,7 @@ fn main() -> ExitCode {
}
})
}
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
Some("recipe") => args.get(1).ok_or_else(usage).and_then(|p| {
let v = read_json(p)?;
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
}),
@@ -130,11 +182,19 @@ fn main() -> ExitCode {
Some("bench-open") => cmd_bench_open(&args[1..]),
// A ~1,300-body design built command by command, as an agent builds one over MCP.
Some("bench-agent") => bench_agent::run(&args[1..]),
// The standard Assistant model test (#4851), run against a desktop app over its control channel.
Some("bench-assistant") => bench_assistant::run(&args[1..]),
Some("--version" | "-V") => {
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
_ => Err(USAGE.to_string()),
// Asked-for help is not an error: usage on stdout, exit 0.
Some("--help" | "-h" | "help") => {
use std::io::Write as _;
let _ = std::io::stdout().lock().write_all(usage().as_bytes());
Ok(())
}
_ => Err(usage()),
};
match r {
Ok(()) => ExitCode::SUCCESS,
@@ -149,6 +209,35 @@ fn pretty(v: &Value) -> String {
serde_json::to_string_pretty(v).unwrap_or_default()
}
/// The whole usage: every command's lines under one `usage:` line. The lines come from the
/// same place a command's own `--help` prints, so the two cannot drift apart.
fn usage() -> String {
let mut s = String::from("usage:\n");
for c in COMMANDS.iter().filter(|c| c.listed) {
s.push_str(c.usage);
}
s.push_str(USAGE_DEBUG);
s
}
/// A command's own usage: its lines under a `usage:` line.
fn command_usage(cmd: &str) -> String {
match COMMANDS.iter().find(|c| c.name == cmd) {
Some(c) => format!("usage:\n{}", c.usage),
None => usage(),
}
}
/// Whether the name is a command the CLI takes.
fn is_command(cmd: &str) -> bool {
COMMANDS.iter().any(|c| c.name == cmd)
}
/// Whether the arguments ask for help.
fn wants_help(args: &[String]) -> bool {
args.iter().any(|a| a == "--help" || a == "-h")
}
pub fn read_json(path: &str) -> Result<Value, String> {
// Through the engine's file access, which refuses anything that is not a regular file and
// reads no more than the cap: a character device or a FIFO would otherwise never end.
@@ -254,7 +343,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
return Err(USAGE.to_string());
return Err(usage());
}
let want_results = args.iter().any(|a| a == "--results");
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
@@ -314,13 +403,14 @@ fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
}
(Some(b), None) => load(b),
(None, Some(p)) => load(p),
(None, None) => Err(USAGE.to_string()),
(None, None) => Err(usage()),
}
}
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
/// command's option, would otherwise be ignored without a word.
fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -> Result<(), String> {
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
fn known_args(cmd: &str, args: &[String], valued: &[&str], switches: &[&str], positionals: bool) -> Result<(), String> {
let mut i = 0;
while let Some(a) = args.get(i) {
if valued.contains(&a.as_str()) {
@@ -328,7 +418,9 @@ fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -
Some(v) if !v.starts_with("--") => i += 2,
_ => return Err(format!("{cmd}: {a} needs a value")),
}
} else if a.starts_with("--") && !switches.contains(&a.as_str()) {
} else if switches.contains(&a.as_str()) {
i += 1;
} else if a.starts_with("--") || !positionals {
return Err(format!("{cmd} doesn't take {a} (it takes {})", [valued, switches].concat().join(", ")));
} else {
i += 1;
@@ -352,7 +444,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(|| USAGE.to_string())?;
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
let mut s = load(path)?;
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
@@ -370,7 +462,7 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
/// A design's history as a command script (`timeline.export_script`): printed, or written to
/// `--out`; what the script leaves out goes to stderr.
fn cmd_script(args: &[String]) -> Result<(), String> {
let path = positional(args).ok_or_else(|| USAGE.to_string())?;
let path = positional(args).ok_or_else(usage)?;
let s = &mut load(path)?;
let out = flag(args, "--out");
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
@@ -431,14 +523,15 @@ fn install_renderer() {
fn cmd_snapshot(args: &[String]) -> Result<(), String> {
install_renderer();
known_flags(
known_args(
"snapshot",
args,
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
&["--no-grid", "--no-axes"],
true,
)?;
if positional(args).is_none() && flag(args, "--in").is_none() {
return Err(USAGE.to_string());
return Err(usage());
}
let out = flag(args, "--out").ok_or("snapshot needs --out FILE.png")?;
let size = |name: &str, default: u32| match flag(args, name) {
@@ -522,7 +615,7 @@ fn ico(images: &[(usize, Vec<u8>)]) -> Result<Vec<u8>, String> {
fn cmd_exec(args: &[String]) -> Result<(), String> {
install_renderer();
let id = args.first().ok_or_else(|| USAGE.to_string())?;
let id = args.first().ok_or_else(usage)?;
let p: Value = match args.get(1) {
Some(t) => serde_json::from_str(t).map_err(|e| format!("params: {e}"))?,
None => json!({}),
@@ -533,10 +626,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
Ok(())
}
/// `mcp setup`: add the server to each installed AI client, with this program's own path.
fn cmd_mcp_setup(args: &[String]) -> Result<(), String> {
use solidcraft_engine::agent_setup as setup;
let (lines, ok) = setup::setup(args, &setup::Env::from_process(), &setup::current_exe()?)?;
for l in lines {
println!("{l}");
}
if ok { Ok(()) } else { Err("mcp setup: some clients could not be configured".into()) }
}
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
fn cmd_mcp(args: &[String]) -> Result<(), String> {
if matches!(args.first().map(String::as_str), Some("setup" | "install")) {
return cmd_mcp_setup(&args[1..]);
}
let render_gpu = args.iter().any(|arg| arg == "--render-gpu");
if render_gpu && flag(args, "--connect").is_some() {
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
}
install_renderer();
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
@@ -580,18 +690,49 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
}
Box::new(remote)
}
(None, Some(path)) => Box::new(solidcraft_mcp::Headless::new(load_checked(path, param_check)?)),
(None, None) => {
let mut headless = solidcraft_mcp::Headless::default();
headless.session.param_check = param_check;
(None, Some(path)) => {
let mut headless = solidcraft_mcp::Headless::new(load_checked(path, param_check)?);
if render_gpu {
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
}
Box::new(headless)
}
(None, None) => {
let auto = !args.iter().any(|a| a == "--headless");
if auto && let Some(port) = discover_app() {
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
return serve_mcp(Box::new(solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token"))), scope);
}
let mut headless = solidcraft_mcp::Headless::default();
headless.session.param_check = param_check;
if render_gpu {
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
}
if auto {
// The app may start after the agent: look for it again until there is work here.
Box::new(solidcraft_mcp::Auto::new(headless, Box::new(discover_app), flag(args, "--token")))
} else {
Box::new(headless)
}
}
};
serve_mcp(backend, scope)
}
fn serve_mcp(backend: Box<dyn solidcraft_mcp::Backend>, scope: Option<solidcraft_engine::io::vfs::PathScope>) -> Result<(), String> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
}
/// The port of the running desktop app: the one its discovery file names, if something listens
/// there (a file left by an app that crashed names a closed port).
fn discover_app() -> Option<u16> {
let port = solidcraft_engine::control_auth::read_current_port()?;
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(500)).ok().map(|_| port)
}
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
/// parameter `save_round` set to the round number each time.
fn cmd_save_stress(args: &[String]) -> Result<(), String> {
@@ -704,9 +845,10 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let t = std::time::Instant::now();
let s = Session::opened(doc, saved.as_ref());
drop(read);
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway).
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
// only the features whose state a thinned file left out.
let adopted = saved.is_some() && s.model.last_recomputed == 0;
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted}));
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted, "recomputed": s.model.last_recomputed}));
for r in &s.model.results {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
+108 -18
View File
@@ -1,25 +1,89 @@
//! Complete headless image-job workflow through the same engine commands as the UI/MCP.
use serde_json::json;
use serde_json::{Value, json};
use solidcraft_engine::{
doc::render_settings::RenderSetup,
render::job::{CpuHost, JobId, Phase},
Session,
doc::render_settings::{RenderJob, RenderSetup},
render::job::{CpuHost, JobId, Phase, ToneMap},
};
/// Only explicit runtime CLI flags are stored here. A typed setup owns these
/// values even when a supplied flag happens to match the saved value.
#[derive(Default)]
struct RuntimeOptions {
progressive: bool,
transparent: bool,
samples: Option<u32>,
max_depth: Option<u32>,
seed: Option<u32>,
lighting: Option<String>,
tone_map: Option<ToneMap>,
environment: Option<String>,
}
impl RuntimeOptions {
fn explicit_flag(&self) -> Option<&'static str> {
[
(self.progressive, "--progressive"),
(self.transparent, "--transparent"),
(self.samples.is_some(), "--samples"),
(self.max_depth.is_some(), "--max-depth"),
(self.seed.is_some(), "--seed"),
(self.lighting.is_some(), "--lighting"),
(self.tone_map.is_some(), "--tone-map"),
(self.environment.is_some(), "--environment"),
]
.into_iter()
.find_map(|(present, flag)| present.then_some(flag))
}
fn request(&self, session: &Session, settings: RenderSetup) -> Result<(Value, bool), String> {
if let Some(job) = &settings.job {
if let Some(flag) = self.explicit_flag() {
return Err(format!("{flag} cannot accompany a typed saved render setup; change the saved job options instead"));
}
settings.validate().map_err(|e| e.to_string())?;
session.doc.render.validate_resources(&settings).map_err(|e| e.to_string())?;
let needs_gpu = matches!(job, RenderJob::GpuPath { .. });
return Ok((json!({"settings":settings}), needs_gpu));
}
// Legacy setups and unnamed requests retain their runtime wire contract.
settings.validate().map_err(|e| e.to_string())?;
let mut params = json!({"settings":settings,"transparent_background":self.transparent,
"progressive":self.progressive,"samples":self.samples,"max_depth":self.max_depth,"seed":self.seed});
if let Some(path) = &self.lighting {
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read lighting: {e}"))?;
let lighting: solidcraft_engine::render::job::StudioLighting = serde_json::from_str(&text).map_err(|e| e.to_string())?;
params["lighting"] = json!(lighting);
}
if let Some(tone_map) = self.tone_map {
params["tone_map"] = json!(tone_map);
}
if let Some(path) = &self.environment {
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read environment: {e}"))?;
params["environment"] = serde_json::from_str(&text).map_err(|e| e.to_string())?;
}
Ok((params, self.progressive))
}
}
pub fn run(args: &[String]) -> Result<(), String> {
let input = args.first().filter(|p| !p.starts_with('-')).ok_or("render requires an input design or script")?;
let mut out = None;
let mut setup = None;
let mut width = None;
let mut height = None;
let mut transparent = false;
let mut exposure = None;
let mut runtime = RuntimeOptions::default();
let mut overwrite = false;
let mut timeout = 120u64;
let mut i = 1;
while let Some(arg) = args.get(i) {
match arg.as_str() {
"--transparent" => transparent = true,
"--progressive" => runtime.progressive = true,
"--transparent" => runtime.transparent = true,
"--overwrite" => overwrite = true,
"--out" | "--setup" | "--width" | "--height" | "--timeout" => {
"--out" | "--setup" | "--width" | "--height" | "--timeout" | "--samples" | "--max-depth" | "--seed" | "--exposure" | "--lighting"
| "--tone-map" | "--environment" => {
i += 1;
let value = args.get(i).ok_or_else(|| format!("missing value for {arg}"))?;
match arg.as_str() {
@@ -27,6 +91,20 @@ pub fn run(args: &[String]) -> Result<(), String> {
"--setup" => setup = Some(value.clone()),
"--width" => width = Some(value.parse::<u32>().map_err(|_| "invalid width")?),
"--height" => height = Some(value.parse::<u32>().map_err(|_| "invalid height")?),
"--samples" => runtime.samples = Some(value.parse::<u32>().map_err(|_| "invalid samples")?),
"--max-depth" => runtime.max_depth = Some(value.parse::<u32>().map_err(|_| "invalid depth")?),
"--seed" => runtime.seed = Some(value.parse::<u32>().map_err(|_| "invalid seed")?),
"--exposure" => exposure = Some(value.parse::<f64>().map_err(|_| "invalid exposure")?),
// Defer file reads until a saved job has rejected conflicting controls.
"--environment" => runtime.environment = Some(value.clone()),
"--lighting" => runtime.lighting = Some(value.clone()),
"--tone-map" => {
runtime.tone_map = Some(match value.as_str() {
"reinhard" => ToneMap::Reinhard,
"aces_fitted" => ToneMap::AcesFitted,
_ => return Err("tone-map must be reinhard or aces_fitted".into()),
})
}
_ => timeout = value.parse::<u64>().map_err(|_| "invalid timeout")?,
}
}
@@ -39,18 +117,10 @@ pub fn run(args: &[String]) -> Result<(), String> {
}
let out = out.ok_or("render requires --out FILE.png")?;
let mut session = crate::load(input)?;
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
let mut settings = if let Some(name) = setup {
session.doc.render.setups.get(&name).cloned().ok_or("no saved render setup with that name")?
let mut settings = if let Some(name) = setup.as_deref() {
session.doc.render.setups.get(name).cloned().ok_or("no saved render setup with that name")?
} else {
let mut settings = RenderSetup::default();
if let Some(camera) = session.camera {
settings.camera = camera;
} else {
settings.camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
}
settings
RenderSetup::default()
};
if let Some(width) = width {
settings.width = width;
@@ -58,7 +128,27 @@ pub fn run(args: &[String]) -> Result<(), String> {
if let Some(height) = height {
settings.height = height;
}
let params = json!({"settings":settings,"transparent_background":transparent});
if let Some(exposure) = exposure {
settings.exposure_ev = exposure;
}
// Resolve saved intent before opening a GPU or explicitly settling geometry.
// Width/height/exposure only change this validated inline settings clone.
let (mut params, needs_gpu) = runtime.request(&session, settings)?;
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
if needs_gpu {
session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
}
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
if setup.is_none() {
let camera = if let Some(camera) = session.camera {
camera
} else {
let mut camera = RenderSetup::default().camera;
camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
camera
};
params["settings"]["camera"] = json!(camera);
}
let preflight = session.execute("render.preflight", &params).map_err(|e| e.to_string())?;
let started = session.execute("render.start", &params).map_err(|e| e.to_string())?;
let id: JobId = serde_json::from_value(started["job"].clone()).map_err(|e| e.to_string())?;
+1 -1
View File
@@ -1412,7 +1412,7 @@ fn a_sketch_on_a_patterned_boss_top() {
fn construction_geometry_and_holes_on_a_moved_component() {
let moved = || {
vec![
json!({"command": "component.create", "params": {"name": "Arm"}}),
json!({"command": "component.create", "params": {"name": "Arm", "grounded": false}}),
json!({"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}}),
json!({"command": "component.activate", "params": {"component": 0}}),
json!({"command": "occurrence.move", "params": {"occurrence": "Arm:1", "translate": [0, 50, 0]}}),
@@ -1,6 +1,6 @@
{"commands": [
{"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
{"command": "component.create", "params": {"name": "Arm"}},
{"command": "component.create", "params": {"name": "Arm", "grounded": false}},
{"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}},
{"command": "solid.fillet", "params": {"edges": [[25, 4, 4]], "radius": 1}},
{"command": "component.activate", "params": {"component": 0}},
+159
View File
@@ -0,0 +1,159 @@
//! An explicit help request prints usage to stdout and succeeds; an unknown command still fails.
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
/// Every command the CLI takes, with the name its own usage names: `inspect` is `eval`, and
/// `licenses` is `licences`.
const COMMANDS: &[(&str, &str)] = &[
("run", "run"),
("eval", "eval"),
("inspect", "eval"),
("script", "script"),
("bench-open", "bench-open"),
("bench-agent", "bench-agent"),
("snapshot", "snapshot"),
("render", "render"),
("exec", "exec"),
("commands", "commands"),
("recipe", "recipe"),
("oracle", "oracle"),
("fuzz", "fuzz"),
("fuzz-one", "fuzz-one"),
("mcp", "mcp"),
("crashes", "crashes"),
("licences", "licences"),
("licenses", "licences"),
("gpu", "gpu"),
("icon", "icon"),
("bench-edit", "bench-edit"),
("save-stress", "save-stress"),
];
/// The commands the usage lists: the aliases answer to another command's name.
const LISTED: &[&str] = &[
"run",
"eval",
"script",
"bench-open",
"bench-agent",
"snapshot",
"render",
"exec",
"commands",
"recipe",
"oracle",
"fuzz",
"mcp",
"crashes",
"licences",
"gpu",
"icon",
];
/// The command names a text mentions after `solidcraft-cli `, each one only once.
fn mentioned(text: &str) -> Vec<&str> {
let mut names: Vec<&str> = text.split("solidcraft-cli ").skip(1).filter_map(|rest| rest.split_whitespace().next()).collect();
names.sort_unstable();
names.dedup();
names
}
/// Runs the binary and waits at most `limit` for it to finish. `None` means it was still
/// running and was killed: the fuzz run `fuzz --help` used to start goes on for minutes.
fn run_within(limit: Duration, args: &[&str]) -> Option<(Option<i32>, String, String)> {
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
let started = Instant::now();
loop {
match child.try_wait().unwrap() {
Some(status) => {
let out = child.wait_with_output().unwrap();
return Some((status.code(), String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned()));
}
None if started.elapsed() >= limit => {
let _ = child.kill();
let _ = child.wait();
return None;
}
None => std::thread::sleep(Duration::from_millis(25)),
}
}
}
#[test]
fn help_prints_usage_and_succeeds() {
for arg in ["--help", "-h", "help"] {
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg(arg).output().unwrap();
assert!(output.status.success(), "{arg}: {}", String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.starts_with("usage:"), "{arg}: {stdout}");
assert!(stdout.contains("solidcraft-cli run"), "{arg}: {stdout}");
assert!(output.stderr.is_empty(), "{arg}: {}", String::from_utf8_lossy(&output.stderr));
}
}
#[test]
fn unknown_command_still_fails_with_usage() {
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("--not-a-command").output().unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).starts_with("error: usage:"));
}
#[test]
fn every_command_prints_its_own_usage() {
for (cmd, named) in COMMANDS {
for flag in ["--help", "-h"] {
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args([*cmd, flag]).output().unwrap();
assert!(output.status.success(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.starts_with("usage:"), "{cmd} {flag}: {stdout}");
assert_eq!(mentioned(&stdout), vec![*named], "{cmd} {flag} printed another command's usage: {stdout}");
assert!(output.stderr.is_empty(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
}
}
}
#[test]
fn the_top_level_usage_still_lists_every_command() {
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("--help").output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
for cmd in LISTED {
assert!(stdout.contains(&format!("solidcraft-cli {cmd}")), "{cmd}: {stdout}");
}
}
#[test]
fn fuzz_help_does_not_start_a_fuzz_run() {
let Some((code, stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", "--help"]) else {
panic!("`fuzz --help` kept running: it started a fuzz run instead of printing the usage");
};
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.starts_with("usage:"), "{stdout}");
assert_eq!(mentioned(&stdout), vec!["fuzz"], "{stdout}");
assert!(stderr.is_empty(), "{stderr}");
}
#[test]
fn fuzz_refuses_an_argument_it_does_not_take() {
for arg in ["--not-a-flag", "stray"] {
let Some((code, _stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", arg]) else {
panic!("`fuzz {arg}` kept running: it started a fuzz run instead of refusing the argument");
};
assert_ne!(code, Some(0), "`fuzz {arg}` succeeded");
assert!(stderr.contains(arg), "`fuzz {arg}`: {stderr}");
}
}
#[test]
fn render_help_keeps_the_registered_gpu_request_controls() {
for args in [vec!["--help"], vec!["render", "--help"]] {
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(&args).output().unwrap();
assert!(output.status.success(), "{args:?}: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.stderr.is_empty());
let stdout = String::from_utf8_lossy(&output.stdout);
for flag in ["--progressive", "--samples", "--max-depth", "--seed", "--exposure", "--lighting", "--environment", "--tone-map"] {
assert!(stdout.contains(flag), "{args:?} lost {flag}: {stdout}");
}
}
}
+174
View File
@@ -89,3 +89,177 @@ fn mcp_connect_without_a_port_is_refused_at_start() {
assert!(stderr.contains("HOST:PORT"), "{addr}: {stderr}");
}
}
/// `mcp` with no arguments finds the running app through the discovery file and uses that
/// launch's token; with no app behind the file it stays a headless session.
#[test]
fn mcp_without_arguments_finds_the_running_app() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let control_dir = std::env::temp_dir().join(format!("solidcraft-mcp-discover-{}-{port}", std::process::id()));
std::fs::create_dir_all(&control_dir).unwrap();
let token = "fedcba9876543210".repeat(4);
std::fs::write(control_dir.join(format!("{port}.token")), &token).unwrap();
std::fs::write(control_dir.join("current.port"), format!("{port}\n")).unwrap();
let app = std::thread::spawn(move || {
// The discovery probe connects and leaves without a request; the tool call follows.
let (stream, line) = loop {
let (stream, _) = listener.accept().unwrap();
let mut line = String::new();
BufReader::new(stream.try_clone().unwrap()).read_line(&mut line).unwrap();
if !line.is_empty() {
break (stream, line);
}
};
let req: Value = serde_json::from_str(&line).unwrap();
let ok = req["token"] == json!(token);
let reply = json!({"id": req["id"], "ok": ok, "result": {"bodies": [], "method": req["method"]}, "error": "unauthorized"});
writeln!(&stream, "{reply}").unwrap();
});
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["mcp", "--allow-any-path"])
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
.env_remove("SOLIDCRAFT_CONTROL_TOKEN")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
writeln!(stdin, "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "measure", "arguments": {}}})).unwrap();
let mut line = String::new();
stdout.read_line(&mut line).unwrap();
let r: Value = serde_json::from_str(&line).unwrap_or_else(|e| panic!("{e}: {line:?}"));
assert_ne!(r["result"]["isError"], true, "{r}");
assert!(r["result"]["content"][0]["text"].as_str().unwrap_or_default().contains("engine.execute"), "{r}");
app.join().unwrap();
drop(stdin);
assert!(child.wait().unwrap().success());
// The file names a port nothing listens on (the app crashed): a headless session instead.
let closed = TcpListener::bind("127.0.0.1:0").unwrap();
let dead = closed.local_addr().unwrap().port();
drop(closed);
std::fs::write(control_dir.join("current.port"), format!("{dead}\n")).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["mcp", "--allow-any-path"])
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
.stdin(Stdio::null())
.output()
.unwrap();
assert!(!String::from_utf8_lossy(&out.stderr).contains("bridged to the running app"), "{}", String::from_utf8_lossy(&out.stderr));
std::fs::remove_dir_all(&control_dir).ok();
}
/// `mcp setup` against a temporary home: the client folders it finds are the only ones it writes.
mod setup {
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::process::Command;
fn home(name: &str) -> PathBuf {
let h = std::env::temp_dir().join(format!("solidcraft-mcp-setup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&h);
std::fs::create_dir_all(&h).unwrap();
h
}
fn run(home: &Path, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["mcp", "setup"])
.args(args)
.env_clear()
.env("HOME", home)
.env("USERPROFILE", home)
.env("APPDATA", home.join("AppData/Roaming"))
.env("LOCALAPPDATA", home.join("AppData/Local"))
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("PATH", "")
.output()
.unwrap()
}
fn json_of(p: &Path) -> Value {
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
}
fn exe() -> String {
let p = Path::new(env!("CARGO_BIN_EXE_solidcraft-cli")).canonicalize().unwrap();
p.to_string_lossy().trim_start_matches(r"\\?\").to_owned()
}
#[test]
fn merges_into_opencode_and_cursor_keeping_other_servers() {
let h = home("merge");
let oc = h.join(".config/opencode");
std::fs::create_dir_all(&oc).unwrap();
let old = json!({"$schema": "https://opencode.ai/config.json", "theme": "x", "mcp": {"other": {"type": "local", "command": ["o"]}}});
std::fs::write(oc.join("opencode.json"), old.to_string()).unwrap();
std::fs::create_dir_all(h.join(".cursor")).unwrap();
std::fs::write(h.join(".cursor/mcp.json"), r#"{"mcpServers": {"other": {"command": "o"}}}"#).unwrap();
let out = run(&h, &["--live"]);
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let v = json_of(&oc.join("opencode.json"));
assert_eq!(v["theme"], "x");
assert_eq!(v["mcp"]["other"], old["mcp"]["other"]);
assert_eq!(v["mcp"]["solidcraft"], json!({"type": "local", "command": [exe(), "mcp"], "enabled": true}));
assert_eq!(v["mcp"]["solidcraft-live"]["command"], json!([exe(), "mcp", "--connect", "127.0.0.1:7878"]));
assert_eq!(json_of(&oc.join("opencode.json.bak")), old);
let c = json_of(&h.join(".cursor/mcp.json"));
assert_eq!(c["mcpServers"]["other"], json!({"command": "o"}));
assert_eq!(c["mcpServers"]["solidcraft"], json!({"command": exe(), "args": ["mcp"]}));
// Codex (TOML) and Claude Code (a big file the CLI owns) are edited in place too.
std::fs::create_dir_all(h.join(".codex")).unwrap();
std::fs::write(h.join(".codex/config.toml"), "# mine\nmodel = \"x\"\n").unwrap();
let cc = "{\n \"numStartups\": 3,\n \"projects\": {}\n}\n";
std::fs::write(h.join(".claude.json"), cc).unwrap();
assert!(run(&h, &[]).status.success());
let toml = std::fs::read_to_string(h.join(".codex/config.toml")).unwrap();
assert!(toml.starts_with("# mine\nmodel = \"x\"\n") && toml.contains("[mcp_servers.solidcraft]"), "{toml}");
let c = json_of(&h.join(".claude.json"));
assert_eq!(c["numStartups"], 3);
assert_eq!(c["mcpServers"]["solidcraft"], json!({"type": "stdio", "command": exe(), "args": ["mcp"]}));
assert_eq!(std::fs::read_to_string(h.join(".claude.json.bak")).unwrap(), cc);
// Running again updates the same entries and leaves one of each.
assert!(run(&h, &[]).status.success());
assert_eq!(json_of(&h.join(".cursor/mcp.json"))["mcpServers"].as_object().unwrap().len(), 3);
}
#[test]
fn dry_run_writes_nothing_and_missing_clients_are_skipped() {
let h = home("dry");
std::fs::create_dir_all(h.join(".cursor")).unwrap();
let out = run(&h, &["--dry-run"]);
assert!(out.status.success());
let text = String::from_utf8_lossy(&out.stdout);
assert!(text.contains("would write") && text.contains("solidcraft"), "{text}");
assert!(!h.join(".cursor/mcp.json").exists());
assert!(!h.join(".config/opencode").exists(), "OpenCode was not installed, so nothing is created for it");
let out = run(&h, &["--client", "claude-desktop"]);
assert!(String::from_utf8_lossy(&out.stdout).contains("not found"));
}
#[test]
fn a_file_that_is_not_json_is_left_alone() {
let h = home("bad");
std::fs::create_dir_all(h.join(".cursor")).unwrap();
std::fs::write(h.join(".cursor/mcp.json"), "{ // comment").unwrap();
let out = run(&h, &[]);
assert!(!out.status.success());
assert_eq!(std::fs::read_to_string(h.join(".cursor/mcp.json")).unwrap(), "{ // comment");
}
#[test]
fn claude_desktop_msix_folder_is_found() {
let h = home("msix");
let pkg = h.join("AppData/Local/Packages/Claude_abc/LocalCache/Roaming/Claude");
std::fs::create_dir_all(&pkg).unwrap();
// The MSIX folder is only read through LOCALAPPDATA, set for every platform here.
assert!(run(&h, &["--client", "claude-desktop"]).status.success());
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
}
}
@@ -0,0 +1,9 @@
//! Explicit native GPU registration must not change bridged MCP ownership.
use std::process::Command;
#[test]
fn connected_application_rejects_headless_gpu_installation_before_connecting() {
let result = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(["mcp", "--render-gpu", "--connect", "127.0.0.1:1"]).output().unwrap();
assert!(!result.status.success());
let message = String::from_utf8_lossy(&result.stderr);
assert!(message.contains("connected application owns its own device"), "{message}");
}
+82 -1
View File
@@ -7,7 +7,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::{ModelBody, ModelState};
use solidcraft_engine::doc::{ModelBody, ModelState, SavedModel};
fn corpus() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
@@ -149,6 +149,87 @@ fn imported_assemblies_open_from_their_model_as_evaluated() {
check("imported hinge", s);
}
/// `built` saved with `saved`, a model of it that keeps only some states (as a file without room
/// for all of them does, #3220), opens as the design evaluated again: evaluating just the
/// features whose state the file left out, then after suppressing a feature at, before or after
/// each state it kept, after undoing that, and with the marker moved there and back.
fn check_thinned(name: &str, built: &Session, saved: &SavedModel) {
let design = built.design();
let bytes =
solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(saved)).unwrap();
assert!(solidcraft_engine::io::read_design_geometry(&bytes).is_some(), "{name}: the file keeps the final state");
let path = temp(&name.replace(' ', "-"));
std::fs::write(&path, &bytes).unwrap();
let mut cached = open(&path);
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
let features = saved.records["features"].as_array().unwrap();
let unkept = features.iter().filter(|f| f["skipped"] == false && f.get("state").is_none()).count();
assert!(unkept > 0, "{name}: some states left out");
assert_eq!(cached.model.recomputed, unkept, "{name}: evaluated just the features whose state the file left out");
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: opened from the file's model");
let n = cached.doc.features.len();
let mut at: Vec<usize> = features
.iter()
.enumerate()
.filter(|(_, f)| f["skipped"] == false && f.get("state").is_some())
.flat_map(|(k, _)| [k.saturating_sub(1), k, k + 1])
.filter(|i| *i < n)
.collect();
at.dedup();
for i in at {
let edit = json!({"feature": cached.doc.features[i].id, "suppressed": true});
let (a, b) = (cached.execute("timeline.suppress", &edit), rebuilt.execute("timeline.suppress", &edit));
assert_eq!(a.is_ok(), b.is_ok(), "{name}: suppressing feature {i}");
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: feature {i} suppressed");
if a.is_ok() {
cached.execute("edit.undo", &json!({})).unwrap();
rebuilt.execute("edit.undo", &json!({})).unwrap();
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: suppressing feature {i} undone");
}
for marker in [json!({"position": i}), json!({})] {
cached.execute("timeline.rollback", &marker).unwrap();
rebuilt.execute("timeline.rollback", &marker).unwrap();
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: the marker at {marker}, from feature {i}");
}
}
let _ = std::fs::remove_file(&path);
}
/// A model too big for its file keeps the final state, then the slowest features' states as fit
/// (#3220). Both the smallest such model (the final state, and states that add no body to it)
/// and one that keeps more open as the design evaluated again. The placed components' design has
/// a cut whose fingerprint reads where the bodies before it lie, which a state left out does not
/// say until it is evaluated.
#[test]
fn thinned_models_open_as_evaluated() {
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/components-arm.solidcraft");
for sample in ["Sample Plate", "Plastic Enclosure", "components-arm"] {
let built = match solidcraft_engine::sample::samples().into_iter().find(|s| s.name == sample) {
Some(s) => {
let mut built = Session::default();
for c in (s.script)()["commands"].as_array().unwrap() {
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
built
}
None => open(&corpus),
};
let saved = built.model.saved(&built.design()).unwrap();
// Every body a megabyte (the records fit in the room left over): each budget keeps one more
// body's worth of states.
let body = 1 << 20;
let pieces: Vec<Vec<(usize, usize)>> = (0..saved.bodies.len()).map(|i| vec![(i, body)]).collect();
let thinned: Vec<SavedModel> =
(0..=saved.bodies.len()).filter_map(|n| saved.thinned(&pieces, n * body + body - 1).map(|(m, _, _)| m)).collect();
let kept = |m: &SavedModel| m.records["features"].as_array().unwrap().iter().filter(|f| f.get("state").is_some()).count();
let smallest = thinned.first().unwrap();
assert!(smallest.records["states"].as_array().unwrap().len() < saved.records["states"].as_array().unwrap().len(), "{sample}");
let more = thinned.iter().find(|m| kept(m) > kept(smallest) && kept(m) < built.doc.features.len()).unwrap();
check_thinned(&format!("{sample} smallest"), &built, smallest);
check_thinned(&format!("{sample} thinned"), &built, more);
}
}
/// Opened from its model, a design evaluates only from the first feature an edit changes: the
/// saved states before it are kept as they were read (the same states, not evaluated again).
#[test]
+250
View File
@@ -59,6 +59,84 @@ fn cli_loads_both_design_encodings_by_contents() {
}
}
/// #1369: a pending concept that did not land in the numbered release must not be silently
/// dropped on open/resave, in either encoding or before replacing an existing destination.
#[test]
fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
use solidcraft_engine::{Session, doc, io};
let dir = Scratch::new();
for version in [33, doc::format::FORMAT] {
let mut experimental = doc::Document::new("Experimental");
experimental.format = doc::format::format_string(version);
experimental.format_pending.push("experimental-card".into());
for (extension, bytes) in [("json", io::write_design(&experimental)), ("solidcraft", io::write_design_container(&experimental).unwrap())] {
let source = dir.0.join(format!("experimental-{version}.{extension}"));
std::fs::write(&source, &bytes).unwrap();
let mut session = Session::default();
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
let original = session.doc.clone();
let original_path = session.path.clone();
let undo = session.undo.len();
let dirty = session.is_dirty();
let err = session.execute("doc.open", &json!({"path": source})).unwrap_err().to_string();
assert!(err.contains("experimental-card") && err.contains("update SolidCraft"), "{err}");
assert_eq!(session.doc, original);
assert_eq!(session.path, original_path);
assert_eq!(session.undo.len(), undo);
assert_eq!(session.is_dirty(), dirty);
for existing in [false, true] {
let destination = dir.0.join(format!("output-{version}-{extension}-{existing}.{extension}"));
let backup = io::backup_path(&destination);
if existing {
std::fs::write(&destination, b"existing design").unwrap();
std::fs::write(&backup, b"existing backup").unwrap();
}
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
assert_eq!(output.status.code(), Some(1));
let error = String::from_utf8_lossy(&output.stderr);
assert!(error.contains("experimental-card") && error.contains("update SolidCraft"), "{error}");
if existing {
assert_eq!(std::fs::read(&destination).unwrap(), b"existing design");
assert_eq!(std::fs::read(&backup).unwrap(), b"existing backup");
} else {
assert!(!destination.exists() && !backup.exists());
}
assert_eq!(std::fs::read(&source).unwrap(), bytes);
}
}
}
}
#[test]
fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
use solidcraft_engine::{doc, io};
let dir = Scratch::new();
let current = doc::Document::new("Supported");
let mut released = current.clone();
released.format = doc::format::format_string(33);
released.format_pending.clear();
let mut experimental = released.clone();
experimental.format_pending.push("reserved-body-names".into());
for (index, mut document) in [current, released, experimental].into_iter().enumerate() {
document.metadata.0.insert("com.example.tool".into(), json!({"format_pending": ["experimental-card"], "physical_card": {"density": 700}}));
for (extension, bytes) in [("json", io::write_design(&document)), ("solidcraft", io::write_design_container(&document).unwrap())] {
let source = dir.0.join(format!("supported-{index}.{extension}"));
let destination = dir.0.join(format!("resaved-{index}.{extension}"));
std::fs::write(&source, &bytes).unwrap();
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let reopened = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
assert_eq!(reopened.metadata, document.metadata);
assert_eq!(reopened.name, document.name);
assert_eq!(reopened.features, document.features);
assert_eq!(std::fs::read(&source).unwrap(), bytes);
}
}
}
#[test]
fn results_still_save_and_export_the_script_once() {
let dir = Scratch::new();
@@ -209,3 +287,175 @@ fn timings_and_profile_report_each_command() {
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
}
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
let path = dir.0.join("render.json");
std::fs::write(
&path,
json!({"commands":[
{"command":"solid.box","params":{"length":10,"width":10,"height":10}},
{"command":"render.settings.set","params":{"name":"Portable setup","set":{"width":16,"height":16,"job":job}}}
]})
.to_string(),
)
.unwrap();
path
}
#[cfg(feature = "raytrace-cpu")]
#[test]
fn render_uses_typed_cpu_setups_from_scripts_and_saved_designs() {
for options in [json!({}), json!({"progressive":{"samples_per_pixel":3,"seed":81},"lights":[]})] {
let dir = Scratch::new();
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":options}));
let image = dir.0.join("script.png");
let result = json_output(
cli()
.arg("render")
.arg(&script)
.args(["--setup", "Portable setup", "--out"])
.arg(&image)
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
.output()
.unwrap(),
);
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-raytrace");
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
assert_eq!(result["preflight"]["request"]["integrator"], "raytrace");
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
let png = image::load_from_memory(&std::fs::read(&image).unwrap()).unwrap();
assert_eq!((png.width(), png.height()), (16, 16));
let saved = dir.0.join("portable.solidcraft");
json_output(cli().arg("run").arg(&script).arg("--save").arg(&saved).output().unwrap());
let original = std::fs::read(&saved).unwrap();
let overridden = dir.0.join("overridden.png");
let result = json_output(
cli()
.arg("render")
.arg(&saved)
.args(["--setup", "Portable setup", "--out"])
.arg(&overridden)
.args(["--width", "12", "--height", "8", "--exposure", "1.25"])
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
.output()
.unwrap(),
);
assert_eq!(result["preflight"]["request"]["width"], 12);
assert_eq!(result["preflight"]["request"]["height"], 8);
assert_eq!(result["preflight"]["request"]["raytrace"]["exposure_ev"], 1.25);
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
let png = image::load_from_memory(&std::fs::read(overridden).unwrap()).unwrap();
assert_eq!((png.width(), png.height()), (12, 8));
assert_eq!(std::fs::read(saved).unwrap(), original, "CLI overrides must not rewrite saved intent");
}
}
#[test]
fn render_preserves_typed_raster_and_legacy_runtime_controls() {
for job in [Value::Null, json!({"method":"raster","transparent_background":true})] {
let dir = Scratch::new();
let script = render_setup_script(&dir, job.clone());
let out = dir.0.join("raster.png");
let mut command = cli();
command
.arg("render")
.arg(&script)
.args(["--setup", "Portable setup", "--out"])
.arg(&out)
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-raster");
if job.is_null() {
command.arg("--transparent");
}
let result = json_output(command.output().unwrap());
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-product");
assert_eq!(result["export"]["frame"]["executor"], "cpu-product");
assert_eq!(result["preflight"]["request"]["transparent_background"], true);
assert!(std::fs::read(out).unwrap().starts_with(b"\x89PNG\r\n\x1a\n"));
}
}
#[test]
fn typed_render_rejects_explicit_runtime_flags_before_gpu_or_option_file_reads() {
let dir = Scratch::new();
let script = render_setup_script(&dir, json!({"method":"gpu_path","options":{"samples":64,"seed":0}}));
let out = dir.0.join("existing.png");
std::fs::write(&out, b"previous completed image").unwrap();
for flags in [
&["--progressive"][..],
&["--transparent"][..],
&["--samples", "64"][..],
&["--max-depth", "8"][..],
&["--seed", "0"][..],
&["--lighting", "deliberately-missing-lighting.json"][..],
&["--tone-map", "reinhard"][..],
&["--environment", "deliberately-missing-environment.json"][..],
] {
let output = cli()
.arg("render")
.arg(&script)
.args(["--setup", "Portable setup", "--out"])
.arg(&out)
.arg("--overwrite")
.args(flags)
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-conflicts")
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let error = String::from_utf8_lossy(&output.stderr);
assert!(error.contains(&format!("{} cannot accompany a typed saved render setup", flags[0])), "{error}");
assert!(output.stdout.is_empty());
assert_eq!(std::fs::read(&out).unwrap(), b"previous completed image");
}
}
#[test]
fn typed_gpu_render_selects_its_host_without_a_progressive_runtime_flag() {
// A deliberately invalid policy proves that dispatch reached the GPU host
// without depending on an adapter or accepting a CPU fallback. The legacy
// --progressive path must still reach that same host-selection boundary.
for job in [json!({"method":"gpu_path","options":{"samples":2}}), Value::Null] {
let dir = Scratch::new();
let script = render_setup_script(&dir, job.clone());
let out = dir.0.join("gpu.png");
let mut command = cli();
command.arg("render").arg(&script).args(["--setup", "Portable setup", "--out"]).arg(&out).env("SOLIDCRAFT_GPU", "not-a-gpu-policy");
if job.is_null() {
command.arg("--progressive");
}
let output = command.output().unwrap();
assert_eq!(output.status.code(), Some(1));
let error = String::from_utf8_lossy(&output.stderr);
assert!(error.contains("SOLIDCRAFT_GPU must be auto, hardware, software or cpu"), "{error}");
assert!(!out.exists());
}
}
#[test]
fn render_setup_overrides_obey_shared_validation_before_host_initialization() {
let dir = Scratch::new();
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":{"progressive":{"samples_per_pixel":64}}}));
let out = dir.0.join("invalid.png");
// 1024 * 1024 * 64 exceeds the unchanged default Compact camera-sample
// budget. Admission must name that envelope before initializing any host.
for (flags, message) in [
(&["--width", "1024", "--height", "1024"][..], "CPU Compact exceeds its 16777216 camera-sample admission"),
(&["--width", "0"][..], "render output must be at least"),
(&["--exposure", "NaN"][..], "render exposure must be finite"),
] {
let output = cli()
.arg("render")
.arg(&script)
.args(["--setup", "Portable setup", "--out"])
.arg(&out)
.args(flags)
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-invalid-settings")
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let error = String::from_utf8_lossy(&output.stderr);
assert!(error.contains(message), "{error}");
assert!(!out.exists());
}
}
+5 -1
View File
@@ -13,6 +13,10 @@ workspace = true
name = "solidcraft-web"
path = "src/main.rs"
[features]
default = []
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
[dependencies]
solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
@@ -30,5 +34,5 @@ web-sys = { version = "0.3", features = [
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"BeforeUnloadEvent", "Event", "EventTarget", "Response", "MediaQueryList", "Gamepad", "Performance",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
] }
+1 -1
View File
@@ -294,7 +294,7 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
let extensions = Array::new();
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf"] {
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm"] {
extensions.push(&e.into());
}
let accept = Object::new();
+4 -1
View File
@@ -499,7 +499,10 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
input.set_type("file");
input.set_accept(".solidcraft,.solvecraft,.json,.step,.stp,.3mf,.stl,.obj,.glb,.gltf");
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
// FreeCAD and Rhino files are part of it.
let accept = solidcraft_engine::io::formats::open_extensions(true).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
input.set_accept(&accept);
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
let _ = input.set_attribute("style", "display:none");
if let Some(body) = doc.body() {
+84 -30
View File
@@ -17,13 +17,21 @@ mod install;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
/// Ctrl+PageDown already reach the browser.)
#[cfg(any(target_arch = "wasm32", test))]
fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
ctrl && key == "Tab"
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
use solidcraft_engine::Session;
use solidcraft_engine::io::vfs;
use solidcraft_ui_egui::{Services, SolveApp};
use solidcraft_ui_egui::{Services, SolveApp, ui_kit};
use wasm_bindgen::JsCast as _;
use crate::api::Api;
@@ -84,6 +92,20 @@ mod web {
on_unload.forget();
}
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
/// the canvas, before eframe sees them; the browser then acts on them as usual.
fn leave_tab_switching_to_browser(window: &web_sys::Window) {
let on_key = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(|e: web_sys::KeyboardEvent| {
if crate::browser_switches_tabs(&e.key(), e.ctrl_key()) {
e.stop_propagation();
}
});
let _ = window.add_event_listener_with_callback_and_bool("keydown", on_key.as_ref().unchecked_ref(), true);
// The listener lives as long as the page.
on_key.forget();
}
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
@@ -109,6 +131,7 @@ mod web {
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
font_language: String,
space_mouse: SpaceMouse,
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
}
impl Shell {
@@ -207,13 +230,15 @@ mod web {
fn sync_recents(&mut self) {
let mut entries = self.files.entries.borrow().clone();
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
let forgotten = self.app.home.forgotten.clone();
let recent = &mut self.app.home.recent;
let known = self.files.disk_known.borrow().clone();
// A design on the computer's disk stays listed while its file is remembered.
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
let p = format!("{}{}", files::OPFS, e.name);
if !recent.contains(&p) {
// A design the user removed from the list is not added back while it is stored.
if !recent.contains(&p) && !forgotten.contains(&p) {
recent.push(p);
}
}
@@ -222,6 +247,10 @@ mod web {
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
disk::prune(keep);
// Drop forgotten entries whose stored design is gone, so the set does not grow without bound.
let stored: std::collections::BTreeSet<String> =
entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)).map(|e| format!("{}{}", files::OPFS, e.name)).collect();
self.app.home.forgotten.retain(|p| stored.contains(p));
}
/// The stored designs: open, insert, rename, delete, download; open from the computer.
@@ -229,24 +258,21 @@ mod web {
if !self.files.panel.get() {
return;
}
let mut open = true;
let mut act: Option<(&'static str, String)> = None;
egui::Window::new("Files in this browser")
.open(&mut open)
.collapsible(false)
.resizable(true)
.default_width(520.0)
.pivot(egui::Align2::CENTER_CENTER)
.default_pos(ctx.content_rect().center())
.show(ctx, |ui| {
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
.cards(3.0)
.show_body(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("Open from computer…").clicked() {
if ui_kit::button(ui, "Open from computer…", ui_kit::Button::Normal).clicked() {
files::pick_from_disk(&self.files, Intent::Open);
}
if ui.button("Insert from computer…").on_hover_text("STEP, 3MF or STL into the open design").clicked() {
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
.on_hover_text("STEP, 3MF or STL into the open design")
.clicked()
{
files::pick_from_disk(&self.files, Intent::Insert);
}
if ui.button("Download open design").clicked() {
if ui_kit::button(ui, "Download open design", ui_kit::Button::Normal).clicked() {
act = Some(("download_open", String::new()));
}
});
@@ -270,7 +296,7 @@ mod web {
for e in &entries {
match &mut self.renaming {
Some((from, to)) if *from == e.name => {
let r = ui.add(egui::TextEdit::singleline(to).desired_width(200.0));
let r = ui_kit::cell_editor(ui, ui.make_persistent_id(("stored_file_name", from.as_str())), to, "", false);
if std::mem::take(&mut self.rename_focus) {
r.request_focus();
}
@@ -288,28 +314,31 @@ mod web {
}
ui.label(format!("{:.1} KB", e.size as f64 / 1024.0));
ui.horizontal(|ui| {
if ui.small_button("Open").clicked() {
if ui_kit::button(ui, "Open", ui_kit::Button::Small).clicked() {
act = Some(("open", e.name.clone()));
}
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design && ui.small_button("Insert").on_hover_text("Into the open design (STEP, 3MF, STL)").clicked() {
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.on_hover_text("Into the open design (STEP, 3MF, STL)")
.clicked()
{
act = Some(("insert", e.name.clone()));
}
if ui.small_button("Rename").clicked() {
if ui_kit::button(ui, "Rename", ui_kit::Button::Small).clicked() {
self.renaming = Some((e.name.clone(), e.name.clone()));
self.rename_focus = true;
}
if ui
.small_button("Save to computer…")
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
.on_hover_text("A copy in a file you choose; the stored design stays here")
.clicked()
{
act = Some(("save_copy", e.name.clone()));
}
if ui.small_button("Download").clicked() {
if ui_kit::button(ui, "Download", ui_kit::Button::Small).clicked() {
act = Some(("download", e.name.clone()));
}
if ui.small_button("Delete").clicked() {
if ui_kit::button(ui, "Delete", ui_kit::Button::Small).clicked() {
act = Some(("delete", e.name.clone()));
}
});
@@ -318,7 +347,7 @@ mod web {
});
});
});
if !open {
if closed {
self.files.panel.set(false);
self.renaming = None;
}
@@ -360,6 +389,8 @@ mod web {
"delete" => {
files::delete(&self.files, &name);
self.app.home.recent.retain(|r| *r != path);
// The file is gone, so there is nothing left to keep forgotten.
self.app.home.forgotten.remove(&path);
}
"rename_cancel" => self.renaming = None,
"rename" => {
@@ -373,6 +404,10 @@ mod web {
*r = new.clone();
}
}
// A forgotten design follows its new name.
if self.app.home.forgotten.remove(&old) {
self.app.home.forgotten.insert(new);
}
}
Err(e) => self.app.set_status(e, true),
}
@@ -386,20 +421,19 @@ mod web {
fn recovery_prompt(&mut self, ctx: &egui::Context) {
let Some((name, path)) = self.files.recovery.borrow().clone() else { return };
let mut answer = None;
egui::Window::new("Recover unsaved work?").collapsible(false).resizable(false).anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]).show(
ctx,
|ui| {
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_recovery"), "restore", "Recover unsaved work?")
.required()
.show_body(ctx, |ui| {
ui.label(format!("“{name}” had unsaved changes when this page last closed."));
ui.horizontal(|ui| {
if ui.button("Recover").clicked() {
if ui_kit::button(ui, "Recover", ui_kit::Button::Normal).clicked() {
answer = Some(true);
}
if ui.button("Discard").clicked() {
if ui_kit::button(ui, "Discard", ui_kit::Button::Normal).clicked() {
answer = Some(false);
}
});
},
);
});
let Some(recover) = answer else { return };
*self.files.recovery.borrow_mut() = None;
if recover {
@@ -488,6 +522,10 @@ mod web {
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
}
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
let now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time));
if let Some(delay) = self.wheel_gestures.update(raw, now) {
ctx.request_repaint_after(delay);
}
files::take_drops(&self.files, raw);
// Before the app's own hook, which adds the control channel's events.
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
@@ -554,6 +592,7 @@ mod web {
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
ask_before_unload(&window);
leave_tab_switching_to_browser(&window);
let Some(document) = window.document() else { return };
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
@@ -651,6 +690,7 @@ mod web {
cjk_bytes: None,
font_language,
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
}))
}),
)
@@ -676,3 +716,17 @@ fn main() {
fn main() {
eprintln!("solidcraft-web only runs in the browser: build it with `trunk build --release` in apps/solidcraft-web");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_ctrl_tab_is_left_to_the_browser() {
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
assert!(browser_switches_tabs("Tab", true));
// Tab and Shift+Tab move between the app's fields; Ctrl with other keys stays the app's.
assert!(!browser_switches_tabs("Tab", false));
assert!(!browser_switches_tabs("s", true));
}
}
+2 -2
View File
@@ -246,11 +246,11 @@ mod tests {
let axis =
|source, rest, full| solidcraft_ui_egui::space_mouse::AxisCalibration { source, reversed: false, rest, dead_zone: 0, full: [full; 2] };
// A short-range device: 15 of its 50 is under the raw dead zone, but 105 of 350 calibrated.
let short = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 0, 50)) };
let short = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 0, 50)) };
let push = Axes { translation: [15, 0, 0], rotation: [0; 3] };
assert!(!push.moving() && moving(push, Some(short)));
// A device resting at 100 on every axis: past the raw dead zone, but at rest calibrated.
let offset = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 100, 350)) };
let offset = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 100, 350)) };
let resting = Axes { translation: [100; 3], rotation: [100; 3] };
assert!(resting.moving() && !moving(resting, Some(offset)));
// Without a calibration, as read.
+18 -5
View File
@@ -7,9 +7,14 @@ license.workspace = true
publish = false
default-run = "solidcraft"
[features]
default = ["raytrace-cpu", "bundled-render-library"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
# (Windows resize border) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
# (Windows resize border, taskbar icon) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
[lints.rust]
unsafe_code = "deny"
@@ -49,9 +54,15 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
# The main window's handle for src/native_frame.rs (already in the tree through eframe and winit).
raw-window-handle = "0.6"
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
# The window's resize border, DPI size and class icon (src/native_frame.rs): the window procedure, hit
# test, DPI, the exe's icon.
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
windows-sys = { version = "0.61", features = [
"Win32_Devices_DeviceAndDriverInstallation",
"Win32_Devices_HumanInterfaceDevice",
"Win32_Devices_Properties",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Diagnostics_Debug",
@@ -65,15 +76,17 @@ windows-sys = { version = "0.61", features = [
] }
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21.2", default-features = false }
# The 3D mouse through the 3Dconnexion driver, which keeps the device to itself (src/space_mouse.rs).
solidcraft-3dx-client = { workspace = true }
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
# driver does not shut the app out.
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
# inset (src/main.rs); all already come with eframe and rfd.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
+334 -25
View File
@@ -3,13 +3,15 @@
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use std::time::Duration;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use solidcraft_engine::MAX_LINE;
use solidcraft_engine::control_auth;
use solidcraft_engine::control_pending;
use solidcraft_ui_egui::ControlRequest;
/// Connections served at once, so a flood must not be able to spawn threads without end.
@@ -18,7 +20,7 @@ const MAX_CONNECTIONS: usize = 16;
const MAX_BAD_LINES: usize = 8;
static OPEN: AtomicUsize = AtomicUsize::new(0);
/// The token files this launch wrote (its `--control` port, the agent chat's endpoint), removed
/// The token files this launch wrote (its control port, the agent chat's endpoint), removed
/// when the app ends.
static TOKEN_FILES: std::sync::Mutex<Vec<std::path::PathBuf>> = std::sync::Mutex::new(Vec::new());
@@ -33,17 +35,45 @@ fn new_token() -> Option<String> {
pub fn remove_token_file() {
if let Ok(files) = TOKEN_FILES.lock() {
for p in files.iter() {
let _ = std::fs::remove_file(p);
control_auth::remove_token_file(p);
}
}
if let Some(port) = APP_PORT.lock().ok().and_then(|p| *p) {
control_auth::remove_current_port(port);
}
}
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
/// The port the app's own channel listens on, for removing the discovery file at exit.
static APP_PORT: std::sync::Mutex<Option<u16>> = std::sync::Mutex::new(None);
pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>();
// Every connection must show the token before anything runs (docs/control-protocol.md).
match control_auth::env_token().or_else(new_token) {
Some(token) => {
listen(port, token, tx, ctx, false);
let listener = match listen_on {
control_auth::Listen::Exact(port) => TcpListener::bind(("127.0.0.1", port)),
control_auth::Listen::Default => control_auth::bind_with_fallback(control_auth::DEFAULT_PORT, control_auth::PORT_TRIES),
};
match listener {
Ok(l) => {
if let Some(port) = listen_on_listener(l, token, tx, ctx, false) {
if matches!(listen_on, control_auth::Listen::Default) {
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
}
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
match control_auth::write_current_port(port) {
Ok(_) => {
if let Ok(mut p) = APP_PORT.lock() {
*p = Some(port);
}
}
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
}
}
}
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
}
}
None => eprintln!("solidcraft: control server not started: no random token available"),
}
@@ -59,20 +89,20 @@ pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u1
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
return None;
};
listen(0, token, tx, ctx, true)
}
/// Listen on `port` (0: a free port the system picks) with `token`; the port it listens on.
/// `patient`: every request may wait as long as a batch (a person decides on it).
fn listen(port: u16, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
let listener = match TcpListener::bind(("127.0.0.1", port)) {
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
Ok(l) => l,
Err(e) => {
eprintln!("solidcraft: control server failed to bind 127.0.0.1:{port}: {e}");
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
return None;
}
};
let port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
listen_on_listener(listener, token, tx, ctx, true)
}
/// Serve `listener` (loopback) with `token`; the port it listens on.
/// `patient`: every request may wait as long as a batch (a person decides on it).
fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
let port = listener.local_addr().ok()?.port();
match control_auth::token_path(port) {
Some(path) => match control_auth::write_token_file(&path, &token) {
Ok(()) => {
@@ -175,14 +205,171 @@ const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::fro
struct Pending {
id: Value,
method: String,
/// The request in messages ("`engine.execute` (solid.fillet)").
what: String,
reply: Receiver<Value>,
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
serve_with(stream, token, tx, ctx, patient, WAITS);
/// Every request the app has been sent and not answered yet, from every connection, for
/// `control.running` (#3654). Each connection's own thread answers that, so it never waits behind
/// a running request.
struct Running {
list: Mutex<Vec<InFlight>>,
connections: AtomicU64,
keys: AtomicU64,
}
fn serve_with(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool, waits: Waits) {
/// What `control.running` says about a request: never its parameters beyond the command id.
struct InFlight {
key: u64,
connection: u64,
id: Value,
method: String,
command: Option<&'static str>,
steps: Option<usize>,
since: Instant,
started: Arc<AtomicBool>,
}
/// The app's requests, from both of its listeners.
static RUNNING: Running = Running::new();
impl Running {
const fn new() -> Self {
Running { list: Mutex::new(Vec::new()), connections: AtomicU64::new(0), keys: AtomicU64::new(0) }
}
fn lock(&self) -> MutexGuard<'_, Vec<InFlight>> {
self.list.lock().unwrap_or_else(PoisonError::into_inner)
}
/// List `req`, sent on `connection`, until the returned ticket is dropped.
fn add(&'static self, connection: u64, id: &Value, req: &ControlRequest) -> Ticket {
let key = self.keys.fetch_add(1, Ordering::Relaxed);
// Ids and methods come from the client: only short ones are repeated to the others.
let id = match id {
Value::Number(_) => id.clone(),
Value::String(s) if s.len() <= 64 => id.clone(),
_ => Value::Null,
};
let p = &req.params;
let command = match req.method.as_str() {
"engine.execute" | "command" => p.get("command").and_then(Value::as_str).and_then(solidcraft_engine::find_command).map(|c| c.id),
_ => None,
};
let steps = match req.method.as_str() {
"engine.batch" | "engine.script" => p.get("commands").and_then(Value::as_array).map(Vec::len),
_ => None,
};
let method = req.method.chars().take(64).collect();
let started = req.started.clone();
self.lock().push(InFlight { key, connection, id, method, command, steps, since: Instant::now(), started });
Ticket { running: self, key }
}
/// The `control.running` result, as `asker`'s connection sees it.
fn result(&self, asker: u64) -> Value {
let rows: Vec<Value> = self
.lock()
.iter()
.map(|e| {
let mut r = json!({
"connection": e.connection,
"id": e.id,
"method": e.method,
"running_s": (e.since.elapsed().as_secs_f64() * 10.0).round() / 10.0,
"queued": !e.started.load(Ordering::Relaxed),
});
if let Some(c) = e.command {
r["command"] = json!(c);
}
if let Some(n) = e.steps {
r["steps"] = json!(n);
}
if e.connection == asker {
r["self"] = json!(true);
}
r
})
.collect();
json!({"running": rows})
}
}
/// A listed request; dropped when its reply arrives.
struct Ticket {
running: &'static Running,
key: u64,
}
impl Drop for Ticket {
fn drop(&mut self) {
let key = self.key;
self.running.lock().retain(|e| e.key != key);
}
}
/// The reply to a timed-out request, received on a thread of its own so the request stays listed
/// until the app answers, also once its connection has closed (#3654).
fn watch(reply: Receiver<Value>, ticket: Ticket) -> Receiver<Value> {
let (late_tx, late_rx) = channel();
// A small thread per timed-out request (each connection has at most one). If none can be
// made, the reply is lost and `control.pending` says the app ended the request without one.
// The reply is passed on before the request leaves the list: once `control.running` no longer
// shows it, its connection's `control.pending` has it.
let _ = std::thread::Builder::new().stack_size(64 * 1024).spawn(move || {
if let Ok(v) = reply.recv() {
let _ = late_tx.send(v);
}
drop(late_tx);
drop(ticket);
});
late_rx
}
/// The reply text for a request that outlasts its wait. A request that only reads changes
/// nothing when it finishes; an export or a save still writes its file, and any other request
/// still applies, as it was not cancelled.
fn timeout_message(what: &str, effect: control_pending::Effect) -> String {
match effect {
control_pending::Effect::ReadsOnly => {
format!("timeout: {what} is still running in the app ({}); ask `control.pending` for its answer", effect.when_done())
}
control_pending::Effect::WritesFile => format!(
"timeout: {what} is still running in the app and will still write the file when it finishes (it was not cancelled: \
don't send it again; the design does not change); ask `control.pending` for its result"
),
control_pending::Effect::Changes => format!(
"timeout: {what} is still running in the app and will still apply when it finishes (it was not cancelled: don't send it \
again); ask `control.pending` for its result"
),
}
}
/// The reply text for a request refused because an earlier one on the connection still runs.
fn busy_message(p: &Pending, method: &str) -> String {
let method: String = method.chars().take(128).collect();
format!(
"busy: request {} ({}) is still running, so `{method}` was not run; ask `control.pending` for that request's result, then \
send this one again",
p.id, p.what
)
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING);
}
fn serve_with(
stream: TcpStream,
token: &str,
tx: Sender<ControlRequest>,
ctx: egui::Context,
patient: bool,
waits: Waits,
running: &'static Running,
) {
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
let Ok(read) = stream.try_clone() else { return };
let mut out = stream;
let mut reader = BufReader::new(read);
@@ -216,13 +403,16 @@ fn serve_with(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: e
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
let params = msg.get("params").cloned().unwrap_or(json!({}));
// A request that timed out may have finished since (#1618).
let late = pending.as_ref().and_then(|p| match p.reply.try_recv() {
// A request that timed out may have finished since (#1618). `control.running` only
// looks, so it leaves that reply where it is.
let late = pending.as_ref().filter(|_| method != "control.running").and_then(|p| match p.reply.try_recv() {
Ok(reply) => Some(reply),
Err(TryRecvError::Disconnected) => Some(json!({"ok": false, "error": "the app ended the request without a reply"})),
Err(TryRecvError::Empty) => None,
});
let mut r = if method == "control.pending" {
let mut r = if method == "control.running" {
json!({"ok": true, "result": running.result(connection)})
} else if method == "control.pending" {
match (pending.take(), late) {
(None, _) => json!({"ok": true, "result": {"pending": null}}),
(Some(p), Some(reply)) => json!({"ok": true, "result": {"pending": p.id, "method": p.method, "done": true, "reply": reply}}),
@@ -234,7 +424,7 @@ fn serve_with(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: e
}
} else if let Some(p) = pending.as_ref().filter(|_| late.is_none()) {
// Nothing more is queued behind it, to run unseen when it ends.
json!({"ok": false, "busy": true, "error": format!("busy: request {} (`{}`) is still running; ask `control.pending` for its result", p.id, p.method)})
json!({"ok": false, "busy": true, "error": busy_message(p, &method)})
} else {
if late.is_some() {
// Finished, and the client moved on without asking: it is dropped.
@@ -242,7 +432,10 @@ fn serve_with(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: e
}
// A batch replaces many short requests: the whole of it may take long (#743).
let wait = if patient || method == "engine.batch" { waits.long } else { waits.short };
let what = control_pending::describe(&method, &params);
let effect = control_pending::effect(&method, &params);
let (req, rrx) = ControlRequest::new(method.clone(), params);
let ticket = running.add(connection, &id, &req);
if tx.send(req).is_err() {
break;
}
@@ -250,8 +443,8 @@ fn serve_with(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: e
match rrx.recv_timeout(wait) {
Ok(r) => r,
Err(_) => {
let r = json!({"ok": false, "running": true, "error": format!("timeout: `{method}` is still running in the app and will still apply; ask `control.pending` for its result")});
pending = Some(Pending { id: id.clone(), method, reply: rrx });
let r = json!({"ok": false, "running": true, "error": timeout_message(&what, effect)});
pending = Some(Pending { id: id.clone(), method, what, reply: watch(rrx, ticket) });
r
}
}
@@ -307,9 +500,10 @@ mod tests {
}
});
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100) };
static RUNNING: Running = Running::new();
std::thread::spawn(move || {
if let Some(stream) = listener.incoming().flatten().next() {
serve_with(stream, token, tx, egui::Context::default(), false, waits);
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING);
}
});
let s = TcpStream::connect(addr).unwrap();
@@ -349,6 +543,121 @@ mod tests {
assert_eq!(*ran.lock().unwrap(), ["slow", "ui.view"], "the busy request never reached the app");
}
/// #3654: `control.running` answers on every connection, at once, with what any connection
/// has the app running or queued (command ids, never parameters); a timed-out request stays
/// listed until the app answers it, and an idle app lists nothing.
#[test]
fn every_connection_sees_what_the_app_is_running() {
static RUNNING: Running = Running::new();
let token = "0123456789abcdef0123456789abcdef";
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
// The window: one request at a time, as `drain_control` takes them; the export is held
// until the test lets it go.
let (gate_tx, gate_rx) = channel::<()>();
std::thread::spawn(move || {
for req in rx {
req.started.store(true, Ordering::Relaxed);
if req.method == "engine.execute" {
let _ = gate_rx.recv();
}
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
});
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60) };
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING));
}
});
let open = || {
let s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
let mut out = s.try_clone().unwrap();
let mut lines = BufReader::new(s);
move |v: Value| -> Value {
writeln!(out, "{v}").unwrap();
let mut l = String::new();
lines.read_line(&mut l).unwrap();
serde_json::from_str(&l).unwrap()
}
};
let running = |ask: &mut dyn FnMut(Value) -> Value, until: &dyn Fn(&[Value]) -> bool| {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let r = ask(json!({"id": 0, "method": "control.running", "token": token}));
let rows = r["result"]["running"].as_array().cloned().unwrap_or_default();
if until(&rows) || Instant::now() > deadline {
return (r, rows);
}
std::thread::sleep(Duration::from_millis(10));
}
};
let mut watcher = open();
let (r, rows) = running(&mut watcher, &|_| true);
assert!(r["ok"] == true && rows.is_empty(), "idle: {r}");
let mut export = open();
let path = "/private/part.step";
let params = json!({"command": "file.export", "params": {"path": path}});
let r = export(json!({"id": 7, "method": "engine.execute", "params": params, "token": token}));
assert_eq!(r["running"], true, "{r}");
// A batch from another connection waits behind the export.
let mut batch = open();
let queued = std::thread::spawn(move || {
let steps = json!([{"command": "solid.box"}, {"command": "solid.box"}]);
batch(json!({"id": "b", "method": "engine.batch", "params": {"commands": steps}, "token": token}))
});
// The window may not have taken the export yet, even though it timed out.
let (r, rows) = running(&mut watcher, &|rows| rows.len() == 2 && rows[0]["queued"] == false);
assert_eq!(rows.len(), 2, "{r}");
let (e, b) = (&rows[0], &rows[1]);
assert_eq!(
(&e["id"], &e["method"], &e["command"], &e["queued"]),
(&json!(7), &json!("engine.execute"), &json!("file.export"), &json!(false)),
"{r}"
);
assert!(e["running_s"].as_f64().is_some_and(|s| s >= 0.1), "{r}");
assert_eq!((&b["id"], &b["method"], &b["steps"], &b["queued"]), (&json!("b"), &json!("engine.batch"), &json!(2), &json!(true)), "{r}");
assert_ne!(e["connection"], b["connection"], "{r}");
assert!(rows.iter().all(|row| row.get("self").is_none()), "{r}");
assert!(!r.to_string().contains(path), "parameters stay with their connection: {r}");
// The connection whose request timed out may ask too, and sees its own marked.
let r = export(json!({"id": 8, "method": "control.running"}));
let own: Vec<&Value> = r["result"]["running"].as_array().unwrap().iter().filter(|row| row["self"] == true).collect();
assert_eq!(own.len(), 1, "{r}");
assert_eq!(own[0]["command"], "file.export", "{r}");
gate_tx.send(()).unwrap();
assert_eq!(queued.join().unwrap()["result"], "engine.batch");
let (r, rows) = running(&mut watcher, &|rows| rows.is_empty());
assert!(rows.is_empty(), "done: {r}");
// Asking `control.running` did not take the export's reply from its connection.
let r = export(json!({"id": 9, "method": "control.pending"}));
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(7), &json!(true)), "{r}");
}
/// A timed-out request that only reads is not said to "still apply", nor is one that only
/// writes a file (an export, #1989); a change is, by name.
#[test]
fn timeout_text_tells_reads_from_changes() {
let says = |method: &str, p: Value| timeout_message(&control_pending::describe(method, &p), control_pending::effect(method, &p));
let read = says("ui.inspect", json!({}));
assert!(read.starts_with("timeout") && read.contains("only reads") && !read.contains("will still apply"), "{read}");
let read = says("engine.execute", json!({"command": "sketch.inspect"}));
assert!(read.contains("only reads") && read.contains("sketch.inspect"), "{read}");
let change = says("engine.execute", json!({"command": "solid.fillet"}));
assert!(change.starts_with("timeout") && change.contains("will still apply") && change.contains("solid.fillet"), "{change}");
let export = says("engine.execute", json!({"command": "file.export", "params": {"path": "/tmp/wall.step"}}));
assert!(export.contains("will still write the file") && !export.contains("apply") && export.contains("file.export"), "{export}");
let save = says("engine.execute", json!({"command": "file.save"}));
assert!(save.contains("will still write the file") && !save.contains("apply"), "{save}");
let render = says("ui.render", json!({"path": "/tmp/a.png"}));
assert!(render.contains("will still write the file"), "{render}");
}
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
/// need not be repeated on that connection.
+91
View File
@@ -0,0 +1,91 @@
//! The graphics device was lost after the window opened: the driver was reset (on Windows a TDR,
//! "Display driver stopped responding and has recovered") or the GPU went away (#4172). A lost
//! device can't draw again, so the window can't show anything more, not even a message. The
//! painter stops drawing (`egui_wgpu::DeviceLost`); the app then closes, keeping unsaved changes
//! for recovery as a forced quit does, and starts again ([`restart`]), and the new start says what
//! happened ([`note`]). A device lost again soon after such a restart starts the next one on the
//! software renderer, which doesn't depend on the graphics driver.
use crate::startup;
/// Set on the start after a lost device: when the device was lost (seconds since 1970).
pub const LOST_ENV: &str = "SOLIDCRAFT_DEVICE_LOST";
/// A loss this recent (seconds) counts for the start after it: its note, and a second loss
/// within it starts on the software renderer. Inherited by later starts, an older stamp is
/// ignored.
const RECENT_SECS: u64 = 600;
/// Seconds since 1970.
pub fn now_secs() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
}
/// Whether `stamp` ([`LOST_ENV`]) names a loss at most [`RECENT_SECS`] before `now`.
pub fn recent(stamp: Option<&str>, now: u64) -> bool {
stamp.and_then(|s| s.trim().parse::<u64>().ok()).is_some_and(|at| at <= now && now - at <= RECENT_SECS)
}
/// Whether the start after a lost device uses the software renderer: when this launch already
/// followed a recent loss (the driver keeps losing it), or ran on it anyway.
pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool {
after_recent_loss || cpu_renderer
}
/// What the start after a lost device tells the user, `software` when it runs on the software
/// renderer.
pub fn note(software: bool) -> String {
let mut note = String::from(
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
);
if software {
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
}
note
}
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
/// renderer when `software`.
pub fn restart(software: bool, lost_at: u64) {
let Ok(exe) = std::env::current_exe() else { return };
let mut command = std::process::Command::new(exe);
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
if software {
command.env(startup::RENDERER_ENV, "cpu");
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
// Not tied to this process: its own process group, no console.
command.creation_flags(0x0000_0200 | 0x0000_0008);
}
if let Err(e) = command.spawn() {
log::error!("graphics: starting again after the device was lost: {e}");
}
}
#[cfg(test)]
mod tests {
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
#[test]
fn only_a_recent_loss_counts() {
let now = 1_000_000;
assert!(super::recent(Some("999990"), now));
assert!(super::recent(Some(" 999400 "), now));
assert!(!super::recent(Some("999399"), now), "older than ten minutes");
assert!(!super::recent(Some("1000001"), now), "from the future");
assert!(!super::recent(Some("soon"), now));
assert!(!super::recent(None, now));
}
/// The first loss starts again on the GPU; a second one soon after, on the software renderer.
#[test]
fn a_second_loss_in_a_row_starts_on_the_software_renderer() {
assert!(!super::restart_on_software(false, false));
assert!(super::restart_on_software(true, false));
assert!(super::restart_on_software(false, true));
assert!(super::note(true).contains("software rendering"));
assert!(!super::note(false).contains("software rendering"));
assert!(super::note(false).contains("kept for recovery"));
}
}
+424 -77
View File
@@ -8,8 +8,10 @@
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
//! `.3mf`/`.stl`/`.obj`/`.glb`/`.gltf` file as a new design holding mesh bodies.
//!
//! `SOLIDCRAFT_SETTINGS_DIR` selects an explicit absolute settings directory without legacy adoption.
//! `--control <port>` (or `SOLIDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
//! server: `{"id":1,"method":"engine.execute","params":{"command":"solid.box","params":{…}}}`.
//! Without them the channel still opens, on the default port, unless Preferences turns it off.
//! See `solidcraft_ui_egui::control` and docs/control-protocol.md.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
@@ -19,42 +21,109 @@
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
mod control_server;
mod device_lost;
#[cfg(target_os = "windows")]
mod native_crash;
#[cfg(target_os = "windows")]
mod native_frame;
#[cfg(target_os = "macos")]
mod native_menu;
#[cfg(target_os = "macos")]
mod open_documents;
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
mod space_mouse;
mod startup;
#[cfg(any(target_os = "windows", test))]
mod window_fit;
use solidcraft_engine::{Session, io};
use solidcraft_ui_egui::{Services, SolveApp};
/// Windows: put the restored window back inside the monitor's work area if it is out of reach
/// (a gone monitor, another scale, a taskbar edge). Sizes are in points, the work area in pixels.
#[cfg(target_os = "windows")]
fn fit_window_to_work_area(ctx: &egui::Context, frame: &eframe::Frame) {
use window_fit::Area;
let Some((x, y, w, h)) = native_frame::work_area(frame) else { return };
let (outer, inner, scale, maximized) = ctx.input(|i| {
let v = i.viewport();
(v.outer_rect, v.inner_rect, v.native_pixels_per_point, v.maximized.unwrap_or(false) || v.fullscreen.unwrap_or(false))
});
let (Some(outer), Some(inner), Some(scale)) = (outer, inner, scale.filter(|s| s.is_finite() && *s > 0.0)) else { return };
if maximized {
return;
}
let work = Area::new(x as f32 / scale, y as f32 / scale, w as f32 / scale, h as f32 / scale);
let now = Area::new(outer.min.x, outer.min.y, outer.width(), outer.height());
let fitted = window_fit::fit(now, work, (960.0, 600.0));
if fitted != now {
// The frame (title bar, borders) keeps its size; only the inner size follows.
let inner_size = egui::vec2((inner.width() - (now.w - fitted.w)).max(1.0), (inner.height() - (now.h - fitted.h)).max(1.0));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(inner_size));
ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(fitted.x, fitted.y)));
}
}
/// `SOLIDCRAFT_PROFILE_STARTUP=1` prints the time since launch at each startup step.
fn mark(step: &str) {
static START: std::sync::OnceLock<(std::time::Instant, bool)> = std::sync::OnceLock::new();
let (start, on) = START.get_or_init(|| (std::time::Instant::now(), solidcraft_engine::geom::env::var_os("PROFILE_STARTUP").is_some()));
if *on {
eprintln!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
}
}
struct App {
solve: SolveApp,
/// The settings folder whose start marker the first frame clears (`startup::started`).
starting: Option<std::path::PathBuf>,
/// Frames begun; the second one means the first was drawn (startup is over).
frames: u32,
/// Started over the control channel: the window doesn't ask for the foreground.
take_focus_unwanted: bool,
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
gpu: Option<LostWatch>,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
space_mouse: space_mouse::SpaceMouse,
#[cfg(target_os = "macos")]
native_menu: Option<native_menu::NativeMenu>,
}
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.frames < 2 {
self.frames += 1;
// Before the window is shown: its title bar must be within reach (window_fit.rs).
#[cfg(target_os = "windows")]
if self.frames == 1 {
fit_window_to_work_area(ctx, _frame);
}
mark(if self.frames == 1 { "first frame begins" } else { "second frame begins (first presented)" });
// A second frame right away, which means the first was presented: the app is up.
ctx.request_repaint();
if self.frames == 2 {
// The window is shown only once its first frame is presented, and Windows then
// keeps a window of a process started from a shortcut or an installer behind the
// others. Ask for the foreground once, now that it is on screen; never again, so
// later frames don't take the focus from whatever the user moved to. Automation
// over the control channel keeps the focus where it is.
if !self.take_focus_unwanted {
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
}
solidcraft_engine::crash::phase(solidcraft_engine::crash::RUNNING);
// A DMG install has no installer step: put solidcraft-cli on PATH, quietly.
#[cfg(target_os = "macos")]
std::thread::spawn(solidcraft_ui_egui::cli_link::install_on_launch);
// Automation runs (a control port was asked for) leave the user's AI apps alone.
if !self.take_focus_unwanted {
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
}
if let Some(dir) = self.starting.take() {
startup::started(&dir);
}
}
}
self.watch_device();
// The design on screen is the one whose commands a crash report replays.
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
@@ -67,6 +136,10 @@ impl eframe::App for App {
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
self.solve.open_path_async(&f);
}
#[cfg(target_os = "macos")]
if let Some(menu) = &mut self.native_menu {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
@@ -76,14 +149,23 @@ impl eframe::App for App {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
#[cfg(target_os = "macos")]
if let Some(menu) = &mut self.native_menu {
menu.raw_input_hook(&mut self.solve, ctx, raw);
}
#[cfg(target_os = "macos")]
macos_fullscreen_content(ctx, raw);
// Before the app's own hook, which adds the control channel's events.
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
// The 3D mouse's buttons, before the app reads the frame: Esc and held modifiers join its input.
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
solidcraft_ui_egui::space_mouse::buttons_hook(&mut self.solve, ctx, self.space_mouse.buttons(), raw);
self.solve.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.solve.ui(ui);
content_ui(&mut self.solve, ui);
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
@@ -93,8 +175,17 @@ impl eframe::App for App {
solidcraft_ui_egui::agent_chat::shutdown(&mut self.solve);
// A save still being written ends before the app does.
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
// Pictures still being drawn off the window thread (Home cards, file previews) end
// before the GPU is torn down: one drawn while the process exits can crash the driver.
if !solidcraft_engine::capture::close(std::time::Duration::from_secs(5)) {
log::warn!("a picture was still being drawn as the app closed");
}
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
solidcraft_engine::crash::end_session();
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
}
// An update asked to restart into the new version or to run its installer.
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
let mut command = std::process::Command::new(&program);
@@ -114,6 +205,79 @@ impl eframe::App for App {
}
}
/// The window's graphics device, watched for its loss.
struct LostWatch {
lost: eframe::egui_wgpu::DeviceLost,
/// Where the next start runs once this device is lost ([`device_lost::restart_on_software`]).
restart_on_software: bool,
/// When the loss was seen: the app is closing to start again.
lost_at: Option<u64>,
}
impl App {
/// The graphics device was lost (#4172): the window can't draw again, so close, keeping
/// unsaved changes for recovery as a forced quit does (the window can't ask about them), and
/// start again once closed ([`device_lost::restart`]).
fn watch_device(&mut self) {
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
let reason = gpu.lost.reason().unwrap_or_default();
log::error!("graphics: the device was lost ({reason}); closing to start again");
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
gpu.lost_at = Some(device_lost::now_secs());
self.solve.quit_requested = true;
}
}
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
/// case, plus FreeCAD's own mixed casing.
fn case_variants(exts: &[&str]) -> Vec<String> {
let mut all: Vec<String> = Vec::new();
for e in exts {
let mut forms = vec![e.to_ascii_lowercase(), e.to_ascii_uppercase()];
if e.eq_ignore_ascii_case("fcstd") {
forms.push("FCStd".into());
}
for v in forms {
if !all.contains(&v) {
all.push(v);
}
}
}
all
}
/// egui's root Ui spans the whole viewport, including native safe areas. Lay out the app
/// inside the unobscured rectangle so its application bar can receive native pointer input.
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
let content = ui.ctx().content_rect();
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
}
/// A transparent macOS title bar still intercepts clicks at the top in fullscreen (#3643).
/// `contentLayoutRect` reports the whole content view once that bar auto-hides, so reserve
/// the native titled-window height even then. AppKit supplies the height in logical points;
/// egui's UI zoom is separate from the display's Retina scale.
#[cfg(target_os = "macos")]
fn macos_fullscreen_content(ctx: &egui::Context, raw: &mut egui::RawInput) {
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
let frame = objc2_app_kit::NSWindow::frameRectForContentRect_styleMask(Default::default(), objc2_app_kit::NSWindowStyleMask::Titled, mtm);
fullscreen_content_insets(raw, frame.size.height as f32, ctx.zoom_factor());
}
#[cfg(any(target_os = "macos", test))]
fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom: f32) {
let fullscreen = raw.viewports.get(&raw.viewport_id).and_then(|v| v.fullscreen).unwrap_or(false);
let mut insets = raw.safe_area_insets.unwrap_or_default();
if fullscreen && title_height.is_finite() && title_height > 0.0 && zoom.is_finite() && zoom > 0.0 {
insets.0.top = insets.0.top.max(title_height / zoom);
}
// Explicitly reset on leaving fullscreen: None would retain the preceding frame's inset.
raw.safe_area_insets = Some(insets);
}
fn services(cc: &eframe::CreationContext<'_>) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
@@ -125,48 +289,14 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
let mut dialog = open_parent.clone();
if !first.is_empty() {
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
let both: Vec<String> = first.iter().flat_map(|e| [e.to_lowercase(), e.to_uppercase()]).collect();
dialog = dialog.add_filter(label, &both);
dialog = dialog.add_filter(label, &case_variants(first));
}
let future = dialog
.add_filter(
"Designs, CAD files and meshes",
&[
io::DESIGN_EXT,
io::LEGACY_DESIGN_EXT,
"json",
"step",
"stp",
"STEP",
"STP",
"igs",
"iges",
"IGS",
"IGES",
"FCStd",
"fcstd",
"3dm",
"3DM",
"3mf",
"3MF",
"stl",
"STL",
"obj",
"OBJ",
"glb",
"GLB",
"gltf",
"GLTF",
],
)
.add_filter("SolidCraft design (container or JSON)", &[io::DESIGN_EXT, io::LEGACY_DESIGN_EXT, "json"])
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
.add_filter("FreeCAD and Rhino", &["FCStd", "fcstd", "3dm", "3DM"])
.add_filter("Mesh (3MF, STL, OBJ, glTF)", &["3mf", "3MF", "stl", "STL", "obj", "OBJ", "glb", "GLB", "gltf", "GLTF"])
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
.add_filter("All files", &["*"])
.pick_file();
// The groups come from the format registry, so a new format joins this dialog by itself.
dialog = dialog.add_filter("Designs, CAD files and meshes", &case_variants(&io::formats::open_extensions(true)));
for (label, exts) in io::formats::import_filters(true) {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
let future = dialog.add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
// One kind of file (the agent chat's attachments): only the types it names, as given and in
@@ -206,6 +336,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
window_system: window_system_now(),
http_get: Some(std::sync::Arc::new(http_get)),
http_download: Some(std::sync::Arc::new(http_download)),
// Already an installed app: only the browser build offers to install.
@@ -284,7 +415,7 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
let agent: ureq::Agent = ureq::Agent::config_builder()
.tls_config(tls)
.http_status_as_error(false)
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 900))))
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 3600))))
.timeout_connect(Some(Duration::from_secs(10)))
.user_agent("SolidCraft")
.max_redirects(0)
@@ -375,14 +506,15 @@ fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Resp
}
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
fn settings_dir() -> Option<std::path::PathBuf> {
eframe::storage_dir("SolidCraft")
fn settings_dir() -> Result<Option<std::path::PathBuf>, String> {
let selected = std::env::var_os("SOLIDCRAFT_SETTINGS_DIR");
startup::settings_directory(selected.as_deref(), eframe::storage_dir("SolidCraft"))
}
/// The anti-aliasing preference, kept next to eframe's storage (it is needed before the
/// window opens).
fn graphics_path() -> Option<std::path::PathBuf> {
settings_dir().map(|d| d.join("graphics.json"))
settings_dir().ok().flatten().map(|d| d.join("graphics.json"))
}
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
@@ -392,7 +524,7 @@ fn graphics_path() -> Option<std::path::PathBuf> {
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
/// are never changed.
fn adopt_legacy_settings() {
let (Some(current), Some(legacy)) = (settings_dir(), eframe::storage_dir("SolveCraft")) else { return };
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(&current);
if let Err(e) = adopt_settings(&current, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
@@ -431,6 +563,18 @@ fn backend_preference() -> Option<String> {
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
}
/// The viewport scene's sample count: the app's anti-aliasing (the window's pass is 1x), clamped
/// by the backend's policy (OpenGL renders 1x, see `effective_msaa_samples`). A count the device
/// cannot render with falls back to the supported default.
fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]) -> u32 {
let requested = eframe::egui_wgpu::effective_msaa_samples(backend, samples.max(1));
if requested <= 1 || supported.is_empty() || supported.contains(&requested) {
requested
} else {
supported.iter().copied().filter(|&n| n <= solidcraft_ui_egui::prefs::Prefs::default().msaa).max().unwrap_or(1)
}
}
/// Anti-aliasing samples from Preferences (4 when unset).
fn msaa() -> u16 {
graphics()
@@ -453,7 +597,8 @@ const FLOAT_DEPTH_BITS: u8 = 32;
const STENCIL_BITS: u8 = 8;
const MSAA: u16 = 4;
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
// Let macOS use the bundle's layered icon; eframe's default bitmap would replace it.
#[cfg(target_os = "macos")]
let icon = egui::IconData::default();
@@ -466,7 +611,7 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
.with_inner_size([1600.0, 1000.0])
.with_min_inner_size([960.0, 600.0])
.with_drag_and_drop(true)
.with_decorations(!CUSTOM_TITLEBAR)
.with_decorations(!custom_titlebar)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false)
@@ -477,7 +622,14 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
const APP_ID: &str = solidcraft_engine::io::APP_ID;
fn main() -> eframe::Result {
adopt_legacy_settings();
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
mark("main");
// Validate before legacy adoption, crash markers or any user preference read.
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
adopt_legacy_settings();
}
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
let mut files = Vec::new();
@@ -514,7 +666,6 @@ fn main() -> eframe::Result {
}
// Settings that don't read are moved aside; launches that never got a window are counted,
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
let settings = settings_dir();
let mut notes: Vec<String> = Vec::new();
let mut failed_starts = 0;
if let Some(dir) = &settings {
@@ -526,6 +677,11 @@ fn main() -> eframe::Result {
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
}
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
// The launch before this one closed because its graphics device was lost (`App::watch_device`).
let after_loss = device_lost::recent(std::env::var(device_lost::LOST_ENV).ok().as_deref(), device_lost::now_secs());
if after_loss {
notes.push(device_lost::note(cpu_renderer));
}
if safe_mode {
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
}
@@ -547,13 +703,21 @@ fn main() -> eframe::Result {
// module on Windows, and on every system the startup step the app was in.
#[cfg(target_os = "windows")]
native_crash::install();
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
let started = match &wgpu_options.wgpu_setup {
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
_ => eframe::wgpu::Backends::all(),
};
let custom_titlebar = CUSTOM_TITLEBAR && !startup::system_title_bar(cfg!(target_os = "windows"), started);
eframe::egui_wgpu::set_startup_hook(startup_step);
solidcraft_engine::crash::phase("window");
let options = eframe::NativeOptions {
viewport: app_viewport()?,
depth_buffer: depth_bits,
stencil_buffer: STENCIL_BITS,
multisampling: samples,
viewport: app_viewport(custom_titlebar)?,
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
depth_buffer: 0,
stencil_buffer: 0,
multisampling: 1,
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
// and writes a scratch copy, leaving the user's settings as they were.
persistence_path: if safe_mode {
@@ -570,12 +734,24 @@ fn main() -> eframe::Result {
#[cfg(all(unix, not(target_os = "macos")))]
let mut options = options;
#[cfg(all(unix, not(target_os = "macos")))]
if prefer_x11(
std::env::var_os("DISPLAY").is_some(),
std::env::var_os("WAYLAND_DISPLAY").is_some(),
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
solidcraft_engine::geom::env::var("WAYLAND").as_deref(),
) {
let x11 = {
// SOLIDCRAFT_WAYLAND wins over Preferences › Graphics › Window system (#3083).
let environment = solidcraft_engine::geom::env::var("WAYLAND");
let preference = window_system_preference();
let wayland_override = environment.as_deref().or(window_system_override(preference.as_deref()));
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
let x11 = prefer_x11(
std::env::var_os("DISPLAY").is_some(),
wayland_display,
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
wayland_override,
);
// Without a Wayland socket winit opens on X11 anyway.
let _ = WINDOW_SYSTEM.set(if x11 || !wayland_display { "x11" } else { "wayland" });
x11
};
#[cfg(all(unix, not(target_os = "macos")))]
if x11 {
options.event_loop_builder = Some(Box::new(|b| {
use winit::platform::x11::EventLoopBuilderExtX11;
b.with_x11();
@@ -587,22 +763,31 @@ fn main() -> eframe::Result {
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let created_in_app = created.clone();
let starting = settings.clone();
mark("run_native");
let result = eframe::run_native(
"SolidCraft",
options,
Box::new(move |cc| {
created_in_app.store(true, std::sync::atomic::Ordering::SeqCst);
solidcraft_engine::crash::phase("app setup");
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
mark("window and device ready");
solidcraft_ui_egui::fonts::setup_bundled_fonts(&cc.egui_ctx);
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
mark("fonts");
let mut app = SolveApp::new(Session::default(), services(cc));
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
app.custom_titlebar = CUSTOM_TITLEBAR;
// Windows: the OS hit test offers the resize border, at the window's DPI, from the
// first message (#1381).
app.custom_titlebar = custom_titlebar;
#[cfg(target_os = "windows")]
{
app.native_resize_border = CUSTOM_TITLEBAR && native_frame::install(cc);
if CUSTOM_TITLEBAR && !app.native_resize_border {
// Windows: the taskbar button shows the mark even when slow first frames keep the
// taskbar waiting for the window's icon.
if !native_frame::set_class_icon(cc) {
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
}
// The OS hit test offers the resize border, at the window's DPI, from the first
// message (#1381).
app.native_resize_border = custom_titlebar && native_frame::install(cc);
if custom_titlebar && !app.native_resize_border {
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
}
}
@@ -611,10 +796,17 @@ fn main() -> eframe::Result {
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
mark("app state and prefs");
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
lost: rs.device_lost.clone(),
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
lost_at: None,
});
if let Some(rs) = &cc.wgpu_render_state {
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
app.set_wgpu(rs, depth_bits, STENCIL_BITS, rs.msaa_samples);
app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
// The graphics driver is loaded now; it may have put its own exception filter
// in front of ours.
@@ -624,12 +816,15 @@ fn main() -> eframe::Result {
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
let info = rs.adapter.get_info();
solidcraft_engine::crash::set_environment(&format!(
"Renderer: {:?} {} ({}), MSAA {}x",
info.backend, info.name, info.driver, rs.msaa_samples
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
info.backend, info.name, info.driver
));
}
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
mark("viewport pipelines");
// `--control`/the environment name a port; otherwise the preference (on by default)
// opens the default one (docs/control-protocol.md).
if let Some(listen_on) = solidcraft_engine::control_auth::plan(control_port, app.preferences.agent_control) {
let rx = control_server::start(listen_on, cc.egui_ctx.clone());
app = app.with_control(rx);
}
// The agent chat's MCP bridge reaches the app through an endpoint of its own, started
@@ -644,6 +839,9 @@ fn main() -> eframe::Result {
app.session.mark_saved();
app.session.undo.clear();
}
// Lock files next to open designs: a design open on another computer says so (#5467),
// the files given at launch included.
solidcraft_ui_egui::open_elsewhere::start(&mut app);
// A launch from Finder: its files arrived before the window.
#[cfg(target_os = "macos")]
let files = {
@@ -680,13 +878,30 @@ fn main() -> eframe::Result {
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
#[cfg(target_os = "macos")]
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
Ok(menu) => Some(menu),
Err(error) => {
app.set_status(format!("System menu: {error}"), true);
None
}
};
#[cfg(target_os = "macos")]
{
app.native_file_menu = native_menu.is_some();
}
mark("app created");
solidcraft_engine::crash::phase("first frame");
Ok(Box::new(App {
solve: app,
starting,
frames: 0,
take_focus_unwanted: control_port.is_some(),
gpu,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
space_mouse,
#[cfg(target_os = "macos")]
native_menu,
}))
}),
);
@@ -717,6 +932,9 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
eprintln!("SolidCraft graphics: starting {backends:?}");
setup.instance_descriptor.backends = backends;
}
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
}
@@ -754,6 +972,36 @@ fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path:
Err(error)
}
/// Linux: the window system this run opened with (`wayland` or `x11`), for Preferences (#3083).
#[cfg(all(unix, not(target_os = "macos")))]
static WINDOW_SYSTEM: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
/// The window system this run uses, where there is a choice (Linux).
fn window_system_now() -> Option<&'static str> {
#[cfg(all(unix, not(target_os = "macos")))]
return WINDOW_SYSTEM.get().copied();
#[cfg(not(all(unix, not(target_os = "macos"))))]
None
}
/// Preferences › Graphics › Window system (`graphics.json`'s `window_system`), read before the
/// window opens.
#[cfg(all(unix, not(target_os = "macos")))]
fn window_system_preference() -> Option<String> {
graphics().and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str).map(str::to_string))
}
/// The window system preference as the `SOLIDCRAFT_WAYLAND` value it stands for: `wayland` is
/// `1`, `x11` is `0`, and `auto` (or anything else) leaves the choice to [`prefer_x11`].
#[cfg(all(unix, not(target_os = "macos")))]
fn window_system_override(preference: Option<&str>) -> Option<&'static str> {
match preference.map(str::trim) {
Some("wayland") => Some("1"),
Some("x11") => Some("0"),
_ => None,
}
}
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
/// on Wayland (only on X11), so dropping files from the file manager showed a "no" cursor: use
/// XWayland when it's there. Except on niri, whose XWayland (xwayland-satellite) breaks window
@@ -791,14 +1039,27 @@ mod backend_tests {
assert!(prefer_x11(true, true, Some("niri"), Some("0")));
assert!(!prefer_x11(false, true, Some("niri"), Some("0")));
}
/// Preferences › Graphics › Window system stands for the same values (#3083).
#[test]
fn the_window_system_preference_maps_to_the_override() {
use super::window_system_override as over;
assert_eq!(
(over(Some("wayland")), over(Some("x11")), over(Some("auto")), over(None), over(Some("junk"))),
(Some("1"), Some("0"), None, None, None)
);
// Wayland chosen on GNOME keeps native Wayland; X11 chosen on niri goes through XWayland.
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
}
}
#[cfg(test)]
mod icon_tests {
mod viewport_tests {
#[cfg(target_os = "macos")]
#[test]
fn macos_preserves_the_bundle_icon_instead_of_setting_a_runtime_bitmap() {
let viewport = super::app_viewport().expect("viewport");
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
assert_eq!(viewport.icon.as_deref(), Some(&eframe::egui::IconData::default()));
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
}
@@ -806,12 +1067,89 @@ mod icon_tests {
#[cfg(not(target_os = "macos"))]
#[test]
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
let viewport = super::app_viewport().expect("viewport");
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
let icon = viewport.icon.expect("running window must have an icon");
assert_eq!((icon.width, icon.height), (64, 64));
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
}
#[test]
fn fullscreen_bar_clears_native_chrome_and_undo_redo_keep_working() {
use egui::{Event, PointerButton, Pos2, RawInput, Rect, ViewportId, vec2};
use serde_json::json;
use solidcraft_ui_egui::scenario::handle_at;
// AppKit points are independent of Retina scale and of the user's UI zoom.
for (zoom, retina) in [(1.0, 1.0), (1.0, 2.0), (1.5, 2.0), (2.0, 2.0)] {
let ctx = egui::Context::default();
solidcraft_ui_egui::fonts::setup_bundled_fonts(&ctx);
ctx.set_zoom_factor(zoom);
let mut app = super::SolveApp::new(super::Session::default(), Default::default());
app.integrated_titlebar = true;
for (command, params) in [
("sketch.create", json!({"plane":"XY"})),
("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})),
("sketch.finish", json!({})),
("solid.extrude", json!({"distance":15})),
] {
app.run(command, params).unwrap();
}
let mut time = 0.0;
let mut frame = |app: &mut super::SolveApp, fullscreen, events| {
time += 0.05;
let mut raw = RawInput {
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
time: Some(time),
events,
..Default::default()
};
let viewport = raw.viewports.get_mut(&ViewportId::ROOT).unwrap();
viewport.fullscreen = Some(fullscreen);
viewport.native_pixels_per_point = Some(retina);
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
};
// Enter, leave, and enter again: no stale inset when returning to a normal window.
for fullscreen in [false, true, false, true] {
frame(&mut app, fullscreen, vec![]);
frame(&mut app, fullscreen, vec![]);
let top = if fullscreen { 32.0 / zoom } else { 0.0 };
for name in ["appbar:file", "appbar:undo", "appbar:redo", "appbar:help"] {
let at = handle_at(name).unwrap();
assert!((at.y - top - 17.0).abs() < 0.1, "{name}: {at:?}, fullscreen={fullscreen}, zoom={zoom}");
}
for (handle, bodies) in [("appbar:undo", 0), ("appbar:redo", 1)] {
let at = handle_at(handle).unwrap();
frame(&mut app, fullscreen, vec![Event::PointerMoved(at)]);
for pressed in [true, false] {
frame(
&mut app,
fullscreen,
vec![Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }],
);
}
assert_eq!(app.session.model.state().bodies.len(), bodies, "{handle}, fullscreen={fullscreen}, zoom={zoom}");
}
}
}
}
#[test]
fn fullscreen_inset_preserves_existing_safe_areas_and_resets_on_exit() {
use egui::epaint::MarginF32;
use egui::{RawInput, SafeAreaInsets, ViewportId};
let mut raw = RawInput::default();
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(true);
raw.safe_area_insets = Some(SafeAreaInsets(MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 }));
super::fullscreen_content_insets(&mut raw, 32.0, 2.0);
assert_eq!(raw.safe_area_insets.unwrap().0, MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 });
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(false);
raw.safe_area_insets = None;
super::fullscreen_content_insets(&mut raw, 32.0, 1.0);
assert_eq!(raw.safe_area_insets, Some(SafeAreaInsets::default()));
}
}
#[cfg(test)]
@@ -848,4 +1186,13 @@ mod msaa_tests {
}
}
}
#[test]
fn the_scene_follows_the_backend_policy_too() {
use eframe::wgpu::Backend;
assert_eq!(super::scene_samples(Backend::Gl, 4, &[1, 4]), 1);
assert_eq!(super::scene_samples(Backend::Gl, 4, &[]), 1);
assert_eq!(super::scene_samples(Backend::Vulkan, 4, &[1, 4]), 4);
assert_eq!(super::scene_samples(Backend::Dx12, 8, &[1, 2, 4]), 4);
}
}
+78 -5
View File
@@ -15,10 +15,14 @@
//! window rescales once at the crossing and stays under the pointer, instead of the OS's move
//! loop and winit disagreeing about a frame the window doesn't have.
//!
//! Replacing a window procedure has no safe API, hence the `unsafe_code` allowance on this
//! module (the app crate denies it; see `native_crash.rs` and `open_documents.rs` for the others).
//! Replacing a window procedure and setting a window class's icon have no safe API, hence the
//! `unsafe_code` allowance on this module (the app crate denies it; see `native_crash.rs` and
//! `open_documents.rs` for the others).
//! The procedure is installed once, on the thread that owns the window, for the main window only,
//! and calls the previous one (winit's) for everything it doesn't answer.
//!
//! [`set_class_icon`] gives the window's class the exe's icon, so the taskbar button shows the
//! mark even when the window is too busy to answer the taskbar's request for its icon.
#![allow(unsafe_code)]
use std::sync::atomic::{AtomicIsize, Ordering};
@@ -26,12 +30,81 @@ use std::sync::atomic::{AtomicIsize, Ordering};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use solidcraft_ui_egui::titlebar;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallWindowProcW, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT,
HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsIconic, IsZoomed, SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SetWindowLongPtrW, WM_GETDPISCALEDSIZE,
WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
CallWindowProcW, GCLP_HICON, GCLP_HICONSM, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT,
HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IMAGE_ICON, IsIconic, IsZoomed, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON,
SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SM_CXSMICON, SetWindowLongPtrW, WM_GETDPISCALEDSIZE, WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
};
// windows-sys has SetClassLongPtrW only on 64-bit targets; on 32-bit Windows it is SetClassLongW.
#[cfg(target_pointer_width = "64")]
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
#[cfg(target_pointer_width = "32")]
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongW as SetClassLongPtrW;
/// The work area (the monitor without the taskbar) of the monitor nearest the window, in physical
/// pixels: left, top, width, height.
pub fn work_area(window: &impl HasWindowHandle) -> Option<(i32, i32, i32, i32)> {
let handle = window.window_handle().ok()?;
let RawWindowHandle::Win32(w) = handle.as_raw() else { return None };
let hwnd = w.hwnd.get() as HWND;
// SAFETY: `hwnd` is the live main window; `info` is a MONITORINFO with its size set, as
// GetMonitorInfoW requires, and the monitor handle comes from the call before it.
let rc = unsafe {
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
let mut info: MONITORINFO = std::mem::zeroed();
info.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
if monitor.is_null() || GetMonitorInfoW(monitor, &mut info) == 0 {
return None;
}
info.rcWork
};
Some((rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top))
}
/// The exe's icon resource (`build.rs` embeds it as `1 ICON`).
const ICON_RESOURCE: usize = 1;
/// Give the main window's class the exe's icon, big and small. Call it before the window is
/// shown (eframe's app creator).
///
/// The taskbar asks a window for its icon when the window appears. eframe sets the window's
/// icon before that, while the window is still hidden, and then shows the window at the end of
/// its first frame; if that frame is slow, or startup kept the thread busy long enough for
/// Windows to mark the window as not responding, the request isn't answered in time. The taskbar then
/// takes the window class's icon and doesn't ask again. winit registers its class without one,
/// so the button showed Windows' generic icon for as long as the window was open.
/// `false` if it isn't a Win32 window or the exe's icon doesn't load; SetClassLongPtrW's own
/// failure can't be told from a class that had no icon (both return 0), so it isn't reported.
pub fn set_class_icon(window: &impl HasWindowHandle) -> bool {
let Ok(handle) = window.window_handle() else { return false };
let RawWindowHandle::Win32(w) = handle.as_raw() else { return false };
let hwnd = w.hwnd.get() as HWND;
// SAFETY: a plain query on the live main window.
let dpi = unsafe { GetDpiForWindow(hwnd) };
let (Some(big), Some(small)) = (exe_icon(SM_CXICON, dpi), exe_icon(SM_CXSMICON, dpi)) else { return false };
// SAFETY: `hwnd` is the live main window and both icons are valid handles. The class keeps
// them for the rest of the process, so they are never destroyed. `as _`: on 32-bit Windows
// this is SetClassLongW (see the imports), which takes an i32.
unsafe {
SetClassLongPtrW(hwnd, GCLP_HICON, big as _);
SetClassLongPtrW(hwnd, GCLP_HICONSM, small as _);
}
true
}
/// The exe's icon at the size of system metric `metric` (`SM_CXICON`, `SM_CXSMICON`) at `dpi`.
fn exe_icon(metric: i32, dpi: u32) -> Option<windows_sys::Win32::Foundation::HANDLE> {
// SAFETY: GetModuleHandleW(null) is this exe, loaded for the life of the process; the name
// is an integer resource id (MAKEINTRESOURCE), which LoadImageW accepts in place of a string.
let icon = unsafe {
let size = GetSystemMetricsForDpi(metric, dpi);
LoadImageW(GetModuleHandleW(std::ptr::null()), std::ptr::without_provenance(ICON_RESOURCE), IMAGE_ICON, size, size, LR_DEFAULTCOLOR)
};
(!icon.is_null()).then_some(icon)
}
/// The window procedure ours replaced (winit's), as an address; 0 before [`install`].
static PREVIOUS: AtomicIsize = AtomicIsize::new(0);
+190
View File
@@ -0,0 +1,190 @@
//! macOS system menus, owned and updated on eframe's main thread.
use std::sync::mpsc::{Receiver, sync_channel};
use muda::{
AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
accelerator::{Accelerator, Code, Modifiers},
};
use solidcraft_ui_egui::{SolveApp, toolbar::FileAction};
pub struct NativeMenu {
menu: Menu,
actions: Vec<(MenuItem, FileAction)>,
help_actions: Vec<(MenuItem, &'static str)>,
events: Receiver<MenuEvent>,
recent: Submenu,
recent_paths: Vec<String>,
}
impl NativeMenu {
pub fn new(app: &SolveApp, ctx: &egui::Context) -> muda::Result<Self> {
let menu = Menu::new();
let application = Submenu::new("SolidCraft", true);
let file = Submenu::new(app.tr("ui.file"), true);
let edit = Submenu::new(app.tr_label("Edit"), true);
let recent = Submenu::new(app.tr("ui.open_recent"), true);
let window = Submenu::new("Window", true);
let help = Submenu::new(app.tr("ui.help"), true);
let mut help_actions = Vec::new();
let mut actions = Vec::new();
let mut add = |parent: &Submenu, label: &str, action: FileAction, shortcut: Option<Accelerator>| -> muda::Result<()> {
let item = MenuItem::new(label, Self::action_enabled(&action, app, ctx), shortcut);
parent.append(&item)?;
actions.push((item, action));
Ok(())
};
application.append(&PredefinedMenuItem::about(
Some("About SolidCraft"),
Some(AboutMetadata { name: Some("SolidCraft".into()), version: Some(env!("CARGO_PKG_VERSION").into()), ..Default::default() }),
))?;
application.append(&PredefinedMenuItem::separator())?;
add(
&application,
&format!("{}\u{2026}", app.tr("ui.preferences")),
FileAction::Settings,
Some(Accelerator::new(Modifiers::META, Code::Comma)),
)?;
add(&application, &format!("{}\u{2026}", app.tr("ui.keyboard_shortcuts")), FileAction::Shortcuts, None)?;
application.append(&PredefinedMenuItem::separator())?;
application.append(&PredefinedMenuItem::services(None))?;
application.append(&PredefinedMenuItem::separator())?;
application.append_items(&[
&PredefinedMenuItem::hide(Some("Hide SolidCraft")),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::separator(),
])?;
add(&application, &format!("{} SolidCraft", app.tr("ui.quit")), FileAction::Quit, Some(Accelerator::new(Modifiers::META, Code::KeyQ)))?;
add(&file, app.tr("ui.new_design"), FileAction::New, Some(Accelerator::new(Modifiers::META, Code::KeyN)))?;
add(&file, &format!("{}\u{2026}", app.tr("ui.open")), FileAction::Open, Some(Accelerator::new(Modifiers::META, Code::KeyO)))?;
file.append(&recent)?;
file.append(&PredefinedMenuItem::separator())?;
for (label, command) in
[("Insert STEP\u{2026}", "file.insert_step"), ("Insert Mesh\u{2026}", "file.insert_mesh"), ("Insert Design", "component.insert_design")]
{
add(&file, app.tr_label(label), FileAction::Command(command), None)?;
}
file.append(&PredefinedMenuItem::separator())?;
add(&file, app.tr("ui.save"), FileAction::Save, Some(Accelerator::new(Modifiers::META, Code::KeyS)))?;
add(
&file,
&format!("{}\u{2026}", app.tr("ui.save_as")),
FileAction::SaveAs,
Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyS)),
)?;
add(&file, app.tr_label("Save As JSON\u{2026}"), FileAction::SaveJson, None)?;
file.append(&PredefinedMenuItem::separator())?;
for (label, action) in [
("Export STL\u{2026}", FileAction::ExportMesh("stl")),
("Export 3MF\u{2026}", FileAction::ExportMesh("3mf")),
("Export STEP\u{2026}", FileAction::Export("step")),
("Export Current View as DXF\u{2026}", FileAction::Command("file.export_view_dxf")),
("Export OBJ\u{2026}", FileAction::Export("obj")),
("Export URDF\u{2026}", FileAction::Export("urdf")),
("Export Design as JSON\u{2026}", FileAction::Export("json")),
] {
add(&file, app.tr_label(label), action, None)?;
}
file.append(&PredefinedMenuItem::separator())?;
add(&file, app.tr_label("Design Properties"), FileAction::Command("document.metadata"), None)?;
add(&edit, app.tr("ui.undo"), FileAction::Undo, Some(Accelerator::new(Modifiers::META, Code::KeyZ)))?;
add(&edit, app.tr("ui.redo"), FileAction::Redo, Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyZ)))?;
add(&window, app.tr_label("Agent Chat"), FileAction::AgentChat, None)?;
window.append(&PredefinedMenuItem::separator())?;
window.append_items(&[
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::bring_all_to_front(None),
])?;
for (action, label) in [
("docs", "Documentation"),
("shortcuts", "Keyboard Shortcuts"),
("messages", "Messages\u{2026}"),
("report", "Report an Issue\u{2026}"),
("crashes", "Crash Reports\u{2026}"),
("install_cli", "Install solidcraft-cli command"),
] {
let item = MenuItem::new(app.tr_label(label), true, None);
help.append(&item)?;
help_actions.push((item, action));
}
menu.append_items(&[&application, &file, &edit, &window, &help])?;
let (sender, events) = sync_channel(64);
let ctx = ctx.clone();
MenuEvent::set_event_handler(Some(move |event| {
let _ = sender.try_send(event);
ctx.request_repaint();
}));
menu.init_for_nsapp();
let mut native = Self { menu, actions, help_actions, events, recent, recent_paths: Vec::new() };
native.update_recent(app)?;
Ok(native)
}
pub fn raw_input_hook(&mut self, app: &mut SolveApp, ctx: &egui::Context, raw: &mut egui::RawInput) {
for event in self.events.try_iter().take(64) {
if let Some((_, action)) = self.help_actions.iter().find(|(item, _)| item.id() == &event.id) {
solidcraft_ui_egui::help::run(app, ctx, action);
}
if let Some((_, action)) = self.actions.iter().find(|(item, _)| item.id() == &event.id) {
if matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() {
raw.events.push(egui::Event::Key {
key: egui::Key::Z,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers { command: true, mac_cmd: true, shift: matches!(action, FileAction::Redo), ..Default::default() },
});
} else {
action.run(app);
}
}
}
}
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
action.enabled(app)
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
for (item, action) in &self.actions {
let enabled = Self::action_enabled(action, app, ctx);
if item.is_enabled() != enabled {
item.set_enabled(enabled);
}
}
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
&& let Err(error) = self.update_recent(app)
{
app.set_status(format!("Recent designs menu: {error}"), true);
}
}
fn update_recent(&mut self, app: &SolveApp) -> muda::Result<()> {
while self.recent.remove_at(0).is_some() {}
self.actions.retain(|(_, action)| !matches!(action, FileAction::OpenRecent(_) | FileAction::ClearRecent));
self.recent_paths = app.home.recent.iter().take(32).cloned().collect();
for path in &self.recent_paths {
let action = FileAction::OpenRecent(path.clone());
let item = MenuItem::new(path, action.enabled(app), None);
self.recent.append(&item)?;
self.actions.push((item, action));
}
self.recent.append(&PredefinedMenuItem::separator())?;
let clear = MenuItem::new(app.tr("ui.clear_recent"), !self.recent_paths.is_empty(), None);
self.recent.append(&clear)?;
self.actions.push((clear, FileAction::ClearRecent));
Ok(())
}
}
impl Drop for NativeMenu {
fn drop(&mut self) {
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
self.menu.remove_for_nsapp();
}
}
File diff suppressed because it is too large Load Diff
+107
View File
@@ -2,13 +2,29 @@
//! its report descriptor and ids under `/sys/class/hidraw/hidrawN/device/`. Reading them is plain
//! file I/O, so no udev or hidapi library is needed.
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Duration;
use solidcraft_space_mouse::{Descriptor, Layout};
const SYSFS: &str = "/sys/class/hidraw";
const DEV: &str = "/dev";
/// `O_NONBLOCK` on Linux's common architectures (x86, ARM, RISC-V). The device is opened with it so
/// a read with nothing to read returns at once and the reader can see whether it should stop.
pub const O_NONBLOCK: i32 = 0o4000;
/// Opens a hidraw node the way the Direct reader reads it: read-only and non-blocking.
pub fn open(path: &Path) -> std::io::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new().read(true).custom_flags(O_NONBLOCK).open(path)
}
/// How long the reader waits when the device has nothing to say, which is also the most a report
/// can be delayed.
const IDLE_POLL: Duration = Duration::from_millis(5);
pub struct Device {
pub path: PathBuf,
pub layout: Layout,
@@ -23,6 +39,30 @@ pub fn find() -> Option<Device> {
names.into_iter().find_map(|name| probe(&Path::new(SYSFS).join(&name), Path::new(DEV).join(&name)))
}
/// Hands every report to `publish` until the device is gone (end of file or an error) or `stop`
/// says so. `source` is non-blocking: when it has nothing, `stop` is asked and the reader waits
/// [`IDLE_POLL`]. Reports are read and handed over exactly as they were with blocking reads.
pub fn read_reports(mut source: impl Read, mut publish: impl FnMut(&[u8]), mut stop: impl FnMut() -> bool) {
let mut report = [0_u8; super::REPORT_BUFFER_BYTES];
loop {
match source.read(&mut report) {
Ok(0) => return,
Ok(size) => publish(report.get(..size).unwrap_or_default()),
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
if stop() {
return;
}
std::thread::sleep(IDLE_POLL);
}
Err(error) => {
log::info!("3D mouse disconnected: {error}");
return;
}
}
}
}
fn probe(sysfs: &Path, path: PathBuf) -> Option<Device> {
let descriptor = Descriptor::parse(&std::fs::read(sysfs.join("device/report_descriptor")).ok()?);
// A controller that declares no motion axis (buttons only, say) isn't read.
@@ -42,6 +82,73 @@ fn ids(uevent: &str) -> Option<(u16, u16)> {
mod tests {
use super::*;
/// A real EACCES, as a user without the udev rule gets: a file of the user's own with no permission
/// bits. Skipped for root, which can open anything.
#[test]
fn a_device_without_permission_is_denied_and_the_daemon_takes_over_within_the_backoff() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if std::fs::metadata("/proc/self").is_ok_and(|meta| meta.uid() == 0) {
return;
}
let dir = std::env::temp_dir().join(format!("sc-hidraw-denied-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let node = dir.join("hidraw0");
std::fs::write(&node, [0_u8; 4]).unwrap();
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o000)).unwrap();
let denied = open(&node);
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(open(&node).is_ok(), "the same node opens once it is readable");
std::fs::remove_dir_all(&dir).ok();
assert_eq!(denied.err().map(|error| error.kind()), Some(std::io::ErrorKind::PermissionDenied));
// The failure is the choice's `since_failed`, with an injected clock: inside the backoff the
// daemon is read, whichever driver was asked for; after it the device is tried again.
use super::super::{Pick, SpaceMouseDriver, UNREADABLE_RETRY, choose, direct_usable};
for wanted in [SpaceMouseDriver::Automatic, SpaceMouseDriver::Spacenavd, SpaceMouseDriver::Direct] {
for ago in [Duration::ZERO, Duration::from_secs(29)] {
assert_eq!(choose(wanted, true, direct_usable(true, Some(ago), UNREADABLE_RETRY)), Some(Pick::Spacenavd), "{wanted:?} {ago:?}");
}
}
assert_eq!(choose(SpaceMouseDriver::Direct, true, direct_usable(true, Some(UNREADABLE_RETRY), UNREADABLE_RETRY)), Some(Pick::Direct));
// And the daemon's own reader delivers over a fake connection.
let (mut daemon, client) = std::os::unix::net::UnixStream::pair().unwrap();
let mut record = [0_u8; 32];
record[4..8].copy_from_slice(&100_i32.to_ne_bytes());
std::io::Write::write_all(&mut daemon, &record).unwrap();
drop(daemon);
let mut seen = 0;
super::super::spacenavd::read(client, |_| seen += 1, |_, _| {}, || false, Duration::from_secs(3600));
assert_eq!(seen, 1);
}
#[test]
fn reports_come_through_idle_gaps_until_the_device_ends_or_a_stop() {
use crate::space_mouse::fake::Script;
let mut script = Script::new();
script.timeout(std::io::ErrorKind::WouldBlock).data(&[1, 2, 3]).timeout(std::io::ErrorKind::WouldBlock).data(&[4]).end();
let (mut seen, mut asked) = (Vec::new(), 0);
read_reports(
script,
|report| seen.push(report.to_vec()),
|| {
asked += 1;
false
},
);
assert_eq!(seen, [vec![1, 2, 3], vec![4]]);
assert_eq!(asked, 2);
let mut script = Script::new();
script.data(&[1]).timeout(std::io::ErrorKind::WouldBlock).data(&[2]);
let mut seen = Vec::new();
read_reports(script, |report| seen.push(report.to_vec()), || true);
assert_eq!(seen, [vec![1]], "a stop at an idle moment ends the read");
// An unplugged device is an error, which ends the read as before.
let mut script = Script::new();
script.data(&[1]).fail(std::io::ErrorKind::BrokenPipe).data(&[2]);
let mut seen = Vec::new();
read_reports(script, |report| seen.push(report.to_vec()), || false);
assert_eq!(seen, [vec![1]]);
}
#[test]
fn ids_come_from_the_uevent() {
let wireless = "DRIVER=hid-generic\nHID_ID=0005:0000256F:0000C63A\nHID_NAME=SpaceMouse Wireless BT\n";
+143 -11
View File
@@ -3,7 +3,8 @@
//! to its clients, so a program that reads the HID device itself bypasses that setup. Over its
//! socket the daemon sends fixed 32-byte records: eight native-endian `i32`, the first the kind of
//! record (0 for motion, then x y z rx ry rz and the milliseconds since the last record).
//! Other records (button presses, from newer daemons also configuration changes) are ignored.
//! Button records (1 pressed, 2 released, then the button's number from 0) are read too; other
//! records (from newer daemons also configuration changes) are ignored.
//!
//! The socket is plain file I/O, so no library is needed, and it needs no udev rule: the daemon
//! is what has access to the device.
@@ -11,6 +12,7 @@
use std::io::Read;
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
use solidcraft_ui_egui::space_mouse::{Axes, FULL_SCALE};
@@ -23,6 +25,11 @@ const RECORD_BYTES: usize = 32;
/// sensitivity). Our axes are in the raw HID units of [`FULL_SCALE`].
const DAEMON_FULL_SCALE: i64 = 500;
/// Whether the daemon's socket is there, without connecting to it.
pub fn exists() -> bool {
SOCKETS.iter().any(|path| Path::new(path).exists())
}
pub fn connect() -> Option<UnixStream> {
SOCKETS.iter().find_map(|path| connect_to(Path::new(path)))
}
@@ -49,6 +56,22 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
})
}
/// The button a button record is about, from 0, and whether it went down; `None` for any other
/// record, or a button past the 32 a bitmask holds.
fn button(record: &[u8; RECORD_BYTES]) -> Option<(u32, bool)> {
let mut data = [0_i32; 2];
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
*slot = i32::from_ne_bytes(*bytes);
}
let [kind, number] = data;
let number = u32::try_from(number).ok().filter(|&n| n < 32)?;
match kind {
1 => Some((number, true)),
2 => Some((number, false)),
_ => None,
}
}
/// A daemon value in our raw units. Out-of-range values stay in `i16`: they are input from another
/// process.
fn unit(value: impl Into<i64>) -> i16 {
@@ -56,18 +79,53 @@ fn unit(value: impl Into<i64>) -> i16 {
i16::try_from(scaled.clamp(i64::from(i16::MIN), i64::from(i16::MAX))).unwrap_or_default()
}
/// Hands every motion record to `publish` until the daemon closes the connection or fails. A
/// partial record at the end is dropped.
pub fn read(mut stream: impl Read, mut publish: impl FnMut(Axes)) {
/// Hands every motion record to `publish` and every button record to `pressed` until the daemon
/// closes the connection or fails, or `stop` says so. A stream with a read timeout
/// (`set_read_timeout`) times out when the daemon is quiet, which is no record and no disconnect:
/// `stop` is asked, and the read goes on, with what of a record had arrived kept. `stop` is also asked
/// at least every `check_every` while records keep arriving, so a daemon that never pauses for that
/// long (a moving cap sends every few milliseconds) does not keep the reader from noticing that the
/// device it prefers is back. A partial record at the end is dropped.
pub fn read(
mut stream: impl Read,
mut publish: impl FnMut(Axes),
mut pressed: impl FnMut(u32, bool),
mut stop: impl FnMut() -> bool,
check_every: Duration,
) {
let mut record = [0_u8; RECORD_BYTES];
let mut filled = 0;
let mut asked = Instant::now();
loop {
match stream.read_exact(&mut record) {
Ok(()) => {
if let Some(axes) = decode(&record) {
publish(axes);
if asked.elapsed() >= check_every {
asked = Instant::now();
if stop() {
return;
}
}
match stream.read(record.get_mut(filled..).unwrap_or_default()) {
Ok(0) => {
log::info!("3D mouse: spacenavd connection ended");
return;
}
Ok(size) => {
filled += size;
if filled == RECORD_BYTES {
filled = 0;
if let Some(axes) = decode(&record) {
publish(axes);
} else if let Some((number, down)) = button(&record) {
pressed(number, down);
}
}
}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(error) if matches!(error.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
asked = Instant::now();
if stop() {
return;
}
}
Err(error) => {
log::info!("3D mouse: spacenavd connection ended: {error}");
return;
@@ -113,6 +171,10 @@ mod tests {
fn other_records_and_hostile_values_are_harmless() {
// A button record (kind 1, press) and one of an unknown kind are not motion.
assert_eq!(decode(&record([1, 0, 1, 0, 0, 0, 0, 0])), None);
// Nor are hostile button numbers a button.
assert_eq!(button(&record([1, -1, 0, 0, 0, 0, 0, 0])), None);
assert_eq!(button(&record([1, i32::MAX, 0, 0, 0, 0, 0, 0])), None);
assert_eq!(button(&record([7, 3, 0, 0, 0, 0, 0, 0])), None);
assert_eq!(decode(&record([7, 500, 500, 500, 500, 500, 500, 0])), None);
// Nothing overflows on the extremes of `i32`.
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
@@ -126,14 +188,84 @@ mod tests {
let (mut daemon, client) = UnixStream::pair().expect("a socket pair");
let mut bytes = Vec::new();
bytes.extend(record([0, 100, 0, 0, 0, 0, 0, 8]));
bytes.extend(record([2, 1, 0, 0, 0, 0, 0, 0])); // a button release: skipped
bytes.extend(record([1, 4, 0, 0, 0, 0, 0, 0])); // button 4 down
bytes.extend(record([2, 4, 0, 0, 0, 0, 0, 0])); // and up
bytes.extend(record([1, 40, 0, 0, 0, 0, 0, 0])); // past the 32 a bitmask holds: skipped
bytes.extend(record([0, 0, 0, 0, 0, 0, 0, 900]));
bytes.extend([1, 2, 3]); // the daemon went away mid-record
daemon.write_all(&bytes).expect("written");
drop(daemon);
let mut seen = Vec::new();
read(client, |axes| seen.push(axes.translation));
let (mut seen, mut buttons) = (Vec::new(), Vec::new());
read(client, |axes| seen.push(axes.translation), |number, down| buttons.push((number, down)), || false, Duration::from_secs(3600));
assert_eq!(seen, [[70, 0, 0], [0, 0, 0]]);
assert_eq!(buttons, [(4, true), (4, false)]);
}
#[test]
fn a_quiet_daemon_times_out_without_losing_the_record_it_was_sending() {
use crate::space_mouse::fake::Script;
use std::io::ErrorKind::{TimedOut, WouldBlock};
let bytes = record([0, 100, 0, 0, 0, 0, 0, 8]);
let mut script = Script::new();
script.timeout(WouldBlock).data(&bytes[..10]).timeout(TimedOut).timeout(WouldBlock).data(&bytes[10..]).timeout(WouldBlock).end();
let (mut seen, mut asked) = (Vec::new(), 0);
read(
script,
|axes| seen.push(axes.translation),
|_, _| {},
|| {
asked += 1;
false
},
Duration::from_secs(3600),
);
assert_eq!(seen, [[70, 0, 0]]);
assert_eq!(asked, 4, "each timeout asks whether to stop");
}
#[test]
fn a_stop_ends_the_read_at_the_next_timeout_only() {
use crate::space_mouse::fake::Script;
let bytes = record([0, 100, 0, 0, 0, 0, 0, 8]);
let mut script = Script::new();
script.data(&bytes).timeout(std::io::ErrorKind::WouldBlock).data(&bytes);
let mut seen = 0;
read(script, |_| seen += 1, |_, _| {}, || true, Duration::from_secs(3600));
assert_eq!(seen, 1, "the record before the timeout arrived; the one after never was read");
}
/// A daemon that never goes quiet (a moving cap sends every few milliseconds) is still asked whether to
/// stop, at the interval given, so the reader can go back to a device that returned.
#[test]
fn a_busy_daemon_is_asked_whether_to_stop_on_a_timer() {
// Ends by itself after half a second, so a read that never asks fails the assertion below
// instead of hanging the test.
struct Busy(Instant);
impl Read for Busy {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.0.elapsed() > Duration::from_millis(500) {
return Ok(0);
}
std::thread::sleep(Duration::from_millis(2));
let bytes = record([0, 100, 0, 0, 0, 0, 0, 8]);
let size = bytes.len().min(buf.len());
buf[..size].copy_from_slice(&bytes[..size]);
Ok(size)
}
}
let (mut seen, mut asked) = (0, 0);
read(
Busy(Instant::now()),
|_| seen += 1,
|_, _| {},
|| {
asked += 1;
asked == 3
},
Duration::from_millis(10),
);
assert_eq!(asked, 3, "asked on the timer although no read ever timed out");
assert!(seen > 3, "records kept coming meanwhile: {seen}");
}
#[test]
@@ -0,0 +1,172 @@
//! Windows: finding 3D mice without asking any other device anything (#2614).
//!
//! hidapi's 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. The
//! reader repeated it every two seconds while no 3D mouse was plugged in, and on some machines the
//! keyboard then kept dropping off the bus and the cursor jumped. Plug and Play already names each
//! interface's top-level usage in its hardware IDs (`HID_DEVICE_UP:0001_U:0008`: Generic Desktop,
//! Multi-Axis Controller), so the reader asks it, which sends nothing to any device, and opens only
//! the interfaces that are 3D mice.
//!
//! The configuration manager has no safe API, hence the `unsafe_code` allowance on this module
//! (the app crate denies it; see `native_crash.rs`).
#![allow(unsafe_code)]
use std::ffi::CString;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
CM_GET_DEVICE_INTERFACE_LIST_PRESENT, CM_Get_DevNode_PropertyW, CM_Get_Device_Interface_List_SizeW, CM_Get_Device_Interface_ListW,
CM_Get_Device_Interface_PropertyW, CM_LOCATE_DEVNODE_NORMAL, CM_Locate_DevNodeW, CONFIGRET, CR_BUFFER_SMALL, CR_SUCCESS,
};
use windows_sys::Win32::Devices::HumanInterfaceDevice::GUID_DEVINTERFACE_HID;
use windows_sys::Win32::Devices::Properties::{
DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING, DEVPROP_TYPE_STRING_LIST, DEVPROPTYPE,
};
use windows_sys::Win32::Foundation::DEVPROPKEY;
/// The hardware ID Windows gives a top-level collection with the Generic Desktop page's
/// Multi-Axis Controller usage.
const MULTI_AXIS_CONTROLLER: &str = "HID_DEVICE_UP:0001_U:0008";
/// Largest property read, in bytes: hardware IDs are a few hundred.
const MAX_PROPERTY_BYTES: u32 = 64 * 1024;
/// Tries at reading the interface list, which can grow between asking its size and reading it.
const LIST_TRIES: usize = 4;
/// The paths of the present HID interfaces whose top-level collection is a multi-axis controller.
pub fn multi_axis_controllers() -> Vec<CString> {
interfaces()
.into_iter()
.filter(|path| {
let Some(instance) = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()) else {
return false;
};
devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids))
})
.filter_map(|path| CString::new(path).ok())
.collect()
}
/// Whether a device with these hardware IDs is a multi-axis controller.
fn is_multi_axis(hardware_ids: &[String]) -> bool {
hardware_ids.iter().any(|id| id.eq_ignore_ascii_case(MULTI_AXIS_CONTROLLER))
}
/// The strings of a nul-separated list (`REG_MULTI_SZ`, or one string), without empty ones.
fn split_multi_sz(buffer: &[u16]) -> Vec<String> {
buffer.split(|c| *c == 0).filter(|s| !s.is_empty()).map(String::from_utf16_lossy).collect()
}
/// `s` as a nul-terminated wide string.
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
/// The paths of the present HID interfaces.
fn interfaces() -> Vec<String> {
for _ in 0..LIST_TRIES {
let mut len = 0u32;
// SAFETY: `len` is a valid place for the size; the class GUID is a static; no device filter.
let sized = unsafe { CM_Get_Device_Interface_List_SizeW(&mut len, &GUID_DEVINTERFACE_HID, null(), CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
if sized != CR_SUCCESS || len == 0 {
return Vec::new();
}
let mut buffer = vec![0u16; len as usize];
// SAFETY: `buffer` holds `len` wide characters, the length the call is given.
let listed =
unsafe { CM_Get_Device_Interface_ListW(&GUID_DEVINTERFACE_HID, null(), buffer.as_mut_ptr(), len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
match listed {
CR_SUCCESS => return split_multi_sz(&buffer),
CR_BUFFER_SMALL => continue,
_ => return Vec::new(),
}
}
Vec::new()
}
/// A string or string-list property, read by `get` (property type, buffer, its size in bytes) in
/// two calls: the size, then the value.
fn property_strings(get: impl Fn(*mut DEVPROPTYPE, *mut u8, *mut u32) -> CONFIGRET) -> Option<Vec<String>> {
let mut kind: DEVPROPTYPE = 0;
let mut size = 0u32;
if get(&mut kind, null_mut(), &mut size) != CR_BUFFER_SMALL || size == 0 || size > MAX_PROPERTY_BYTES {
return None;
}
let mut buffer = vec![0u16; (size as usize).div_ceil(2)];
let mut size = (buffer.len() * 2) as u32;
if get(&mut kind, buffer.as_mut_ptr().cast(), &mut size) != CR_SUCCESS || (kind != DEVPROP_TYPE_STRING && kind != DEVPROP_TYPE_STRING_LIST) {
return None;
}
Some(split_multi_sz(&buffer))
}
/// A string property of the device interface at `path`.
fn interface_strings(path: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
let path = wide(path);
property_strings(|kind, buffer, size| {
// SAFETY: `path` is nul-terminated and outlives the call; `buffer` is null (asking the size)
// or holds `*size` bytes, as `property_strings` sets up.
unsafe { CM_Get_Device_Interface_PropertyW(path.as_ptr(), key, kind, buffer, size, 0) }
})
}
/// A string property of the device with instance ID `instance`.
fn devnode_strings(instance: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
let instance = wide(instance);
let mut devnode = 0u32;
// SAFETY: `instance` is nul-terminated and outlives the call; `devnode` is a valid place for the result.
if unsafe { CM_Locate_DevNodeW(&mut devnode, instance.as_ptr(), CM_LOCATE_DEVNODE_NORMAL) } != CR_SUCCESS {
return None;
}
property_strings(|kind, buffer, size| {
// SAFETY: `buffer` is null (asking the size) or holds `*size` bytes, as `property_strings` sets up.
unsafe { CM_Get_DevNode_PropertyW(devnode, key, kind, buffer, size, 0) }
})
}
#[cfg(test)]
mod tests {
use super::*;
/// The hardware IDs Windows gives a wired 3Dconnexion device's motion interface, and a
/// keyboard's: only the first is a multi-axis controller.
#[test]
fn only_the_multi_axis_usage_is_a_3d_mouse() {
let ids = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let space_mouse = ids(&["HID\\VID_256F&PID_C635&REV_0437&MI_00", "HID\\VID_256F&UP:0001_U:0008", "HID_DEVICE_UP:0001_U:0008", "HID_DEVICE"]);
let keyboard = ids(&[
"HID\\VID_046D&PID_C31C&REV_6400&MI_00",
"HID\\VID_046D&UP:0001_U:0006",
"HID_DEVICE_SYSTEM_KEYBOARD",
"HID_DEVICE_UP:0001_U:0006",
"HID_DEVICE",
]);
let vendor = ids(&["HID\\VID_256F&UP:FF00_U:0001", "HID_DEVICE_UP:FF00_U:0001", "HID_DEVICE_UPR:FF00-FFFF", "HID_DEVICE"]);
assert!(is_multi_axis(&space_mouse));
assert!(is_multi_axis(&ids(&["hid_device_up:0001_u:0008"])), "IDs compare without regard to case");
assert!(!is_multi_axis(&keyboard) && !is_multi_axis(&vendor) && !is_multi_axis(&[]));
}
#[test]
fn nul_separated_lists_split_into_their_strings() {
let buffer: Vec<u16> = "a\\b\0HID_DEVICE\0\0".encode_utf16().collect();
assert_eq!(split_multi_sz(&buffer), ["a\\b", "HID_DEVICE"]);
assert_eq!(split_multi_sz(&wide("one")), ["one"]);
assert!(split_multi_sz(&[0, 0]).is_empty() && split_multi_sz(&[]).is_empty());
}
/// The scan asks Plug and Play only: on any machine it returns, and every path it returns is a
/// present HID interface whose hardware IDs name the multi-axis usage.
#[test]
fn the_scan_returns_only_multi_axis_interfaces() {
let found = multi_axis_controllers();
let all = interfaces();
for path in &found {
let path = path.to_str().unwrap_or_default();
assert!(all.iter().any(|p| p == path), "{path}");
let instance = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()).unwrap_or_default();
assert!(devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids)), "{path}");
}
eprintln!("{} HID interfaces present, {} multi-axis controllers", all.len(), found.len());
}
}
+80
View File
@@ -21,6 +21,25 @@ pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
const MARKER: &str = "starting";
/// Explicit application-settings isolation for side-by-side builds and native UI
/// acceptance. A supplied invalid path refuses startup rather than falling back
/// to the user's ordinary settings. No directory is read or created here.
pub fn settings_directory(override_path: Option<&std::ffi::OsStr>, default: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
match override_path {
None => Ok(default),
Some(raw) => {
let path = PathBuf::from(raw);
if raw.is_empty() || !path.is_absolute() {
return Err("SOLIDCRAFT_SETTINGS_DIR must be a nonempty absolute directory path".into());
}
if path.exists() && !path.is_dir() {
return Err("SOLIDCRAFT_SETTINGS_DIR names a file, not a directory".into());
}
Ok(Some(path))
}
}
}
/// Whether an environment value switches something on.
pub fn env_on(v: Option<&str>) -> bool {
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
@@ -189,6 +208,29 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
}
/// The environment variable that switches wgpu's indirect-call validation on or off.
pub const INDIRECT_VALIDATION_ENV: &str = "WGPU_VALIDATION_INDIRECT_CALL";
/// The instance flags for the window. wgpu's indirect-call validation compiles a compute shader
/// when the device is created, and on Intel Macs (macOS 12 with Intel Iris graphics, #3024,
/// #3080, #4605) the Metal compiler refuses it: the device is lost and the window never opens.
/// So on an Intel Mac the validation is left off, unless the user turned it on or off with
/// `WGPU_VALIDATION_INDIRECT_CALL` themselves.
pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool, env: Option<&str>) -> eframe::wgpu::InstanceFlags {
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
}
/// Windows with OpenGL: the window keeps the system title bar (#2063). The window without OS
/// decorations keeps its caption style while having no frame, and some OpenGL drivers (older
/// Intel HD/UHD) then show the picture a caption's height lower than the window, so it no longer
/// lines up with the pointer. Direct3D 12 and Vulkan draw it in place and keep the application bar
/// as the title bar. `backends` are the ones wgpu starts: OpenGL alone is decided before the
/// window opens.
pub fn system_title_bar(windows: bool, backends: eframe::wgpu::Backends) -> bool {
use eframe::wgpu::Backends;
windows && backends.contains(Backends::GL) && !backends.intersects(Backends::DX12 | Backends::VULKAN)
}
/// The message shown when no window could be opened at all.
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
@@ -214,6 +256,32 @@ mod tests {
d
}
#[test]
fn isolated_settings_are_explicit_absolute_and_never_fall_back() {
let default = std::env::temp_dir().join("ordinary-settings");
assert_eq!(settings_directory(None, Some(default.clone())).unwrap(), Some(default.clone()));
let selected = std::env::temp_dir().join("solidcraft-isolated-settings-fixture");
assert_eq!(settings_directory(Some(selected.as_os_str()), Some(default.clone())).unwrap(), Some(selected));
for raw in ["", "relative/settings", "../ordinary"] {
assert!(settings_directory(Some(std::ffi::OsStr::new(raw)), Some(default.clone())).is_err());
}
let file = std::env::current_exe().unwrap();
assert!(settings_directory(Some(file.as_os_str()), Some(default)).is_err());
}
#[test]
fn intel_macs_start_without_indirect_call_validation() {
use eframe::wgpu::InstanceFlags as F;
let on = F::VALIDATION | F::VALIDATION_INDIRECT_CALL;
assert_eq!(window_instance_flags(on, true, None), F::VALIDATION);
assert_eq!(window_instance_flags(on, true, Some(" ")), F::VALIDATION);
// The user's own choice stands, on or off.
assert_eq!(window_instance_flags(on, true, Some("1")), on);
assert_eq!(window_instance_flags(F::VALIDATION, true, Some("0")), F::VALIDATION);
// Every other machine keeps what it had.
assert_eq!(window_instance_flags(on, false, None), on);
}
#[test]
fn switches_read_from_the_environment() {
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
@@ -290,6 +358,18 @@ mod tests {
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
}
#[test]
fn only_opengl_on_windows_gets_the_system_title_bar() {
use eframe::wgpu::Backends;
assert!(system_title_bar(true, Backends::GL));
for b in [Backends::DX12, Backends::VULKAN, Backends::DX12 | Backends::GL, Backends::VULKAN | Backends::GL, Backends::all()] {
assert!(!system_title_bar(true, b), "{b:?}");
}
for b in [Backends::GL, Backends::VULKAN, Backends::METAL] {
assert!(!system_title_bar(false, b), "{b:?}");
}
}
#[test]
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
use eframe::wgpu::Backends;
+117
View File
@@ -0,0 +1,117 @@
//! Keeping the restored window reachable.
//!
//! The window comes back where it was left. After a monitor was unplugged, the scale changed or
//! the settings came from a larger screen, that can put its title bar above the top edge or
//! under the taskbar, out of reach of the mouse. [`fit`] pulls a window back into the monitor's
//! work area (the screen without the taskbar), in whatever unit the caller measures both, as
//! long as it is the same one.
/// A rectangle: left, top, width, height.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Area {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Area {
pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Area { x, y, w, h }
}
fn right(self) -> f32 {
self.x + self.w
}
fn bottom(self) -> f32 {
self.y + self.h
}
fn sane(self) -> bool {
[self.x, self.y, self.w, self.h].iter().all(|v| v.is_finite()) && self.w > 0.0 && self.h > 0.0
}
}
/// How much of the title bar must stay over the work area at least, to grab it.
pub const REACHABLE: f32 = 100.0;
/// The size a window gets when its saved geometry is unusable, before it is fitted to the screen.
pub const DEFAULT_SIZE: (f32, f32) = (1600.0, 1000.0);
/// `window` moved and shrunk so that it can be reached on `work`: no larger than the work area
/// (but not below `min`), the title bar not above the work area's top, the window not below its
/// bottom, and at least [`REACHABLE`] of it over the work area horizontally. A window that is
/// fine is returned unchanged. Geometry that makes no sense (not finite, empty), or a work
/// area that does not, gives the default size centred on the work area.
pub fn fit(window: Area, work: Area, min: (f32, f32)) -> Area {
if !work.sane() {
return window;
}
let min = (min.0.min(work.w), min.1.min(work.h));
if !window.sane() {
let w = DEFAULT_SIZE.0.min(work.w * 0.9).max(min.0);
let h = DEFAULT_SIZE.1.min(work.h * 0.9).max(min.1);
return Area::new(work.x + (work.w - w) / 2.0, work.y + (work.h - h) / 2.0, w, h);
}
let w = window.w.clamp(min.0, work.w);
let h = window.h.clamp(min.1, work.h);
let reach = REACHABLE.min(w);
// A window that had to shrink goes fully onto the screen; one that fits may keep straddling two.
let x = if w < window.w { window.x.max(work.x).min(work.right() - w) } else { window.x.max(work.x + reach - w).min(work.right() - reach) };
let y = window.y.min(work.bottom() - h).max(work.y);
Area::new(x, y, w, h)
}
#[cfg(test)]
mod tests {
use super::*;
const WORK: Area = Area::new(0.0, 0.0, 2560.0, 1400.0);
const MIN: (f32, f32) = (960.0, 600.0);
#[test]
fn a_window_on_screen_is_left_alone() {
let w = Area::new(300.0, 120.0, 1600.0, 1000.0);
assert_eq!(fit(w, WORK, MIN), w);
}
#[test]
fn a_title_bar_above_the_top_or_under_the_taskbar_comes_back() {
let fitted = fit(Area::new(300.0, -400.0, 1600.0, 1000.0), WORK, MIN);
assert_eq!((fitted.y, fitted.h), (0.0, 1000.0));
let fitted = fit(Area::new(300.0, 1300.0, 1600.0, 1000.0), WORK, MIN);
assert_eq!(fitted.y + fitted.h, 1400.0);
// A taskbar at the top moves the work area down.
let below_bar = Area::new(0.0, 48.0, 2560.0, 1352.0);
assert_eq!(fit(Area::new(300.0, 0.0, 1600.0, 1000.0), below_bar, MIN).y, 48.0);
}
#[test]
fn a_window_from_a_monitor_that_is_gone_keeps_a_grabbable_part_on_screen() {
let right = fit(Area::new(5000.0, 100.0, 1600.0, 1000.0), WORK, MIN);
assert_eq!(right.x, 2560.0 - REACHABLE);
let left = fit(Area::new(-4000.0, 100.0, 1600.0, 1000.0), WORK, MIN);
assert_eq!(left.x + left.w, REACHABLE);
}
#[test]
fn a_window_larger_than_the_screen_shrinks_to_it() {
let small = Area::new(0.0, 0.0, 1280.0, 720.0);
let fitted = fit(Area::new(-20.0, -30.0, 3200.0, 2000.0), small, MIN);
assert_eq!(fitted, small);
// Never below the minimum, unless the screen itself is smaller.
assert_eq!(fit(Area::new(0.0, 0.0, 100.0, 100.0), WORK, MIN).w, 960.0);
assert_eq!(fit(Area::new(0.0, 0.0, 100.0, 100.0), Area::new(0.0, 0.0, 800.0, 500.0), MIN).h, 500.0);
}
#[test]
fn unusable_geometry_is_centred_at_a_sensible_size() {
for bad in [Area::new(f32::NAN, 0.0, 1600.0, 1000.0), Area::new(0.0, 0.0, 0.0, 0.0), Area::new(0.0, 0.0, f32::INFINITY, 10.0)] {
let fitted = fit(bad, WORK, MIN);
assert_eq!(fitted, Area::new(480.0, 200.0, 1600.0, 1000.0));
}
let small = Area::new(100.0, 50.0, 1366.0, 728.0);
let fitted = fit(Area::new(f32::NAN, 0.0, 1.0, 1.0), small, MIN);
assert!(fitted.w <= small.w && fitted.h <= small.h && fitted.x >= small.x && fitted.y >= small.y);
// No work area to go by: nothing to change.
let w = Area::new(1.0, 2.0, 3.0, 4.0);
assert_eq!(fit(w, Area::new(0.0, 0.0, 0.0, 0.0), MIN), w);
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
## Code and provenance
Resources remain compiled by their owning layer through existing APIs:
`ui-egui::icons::paint` and `Tokens` at L5, model presets in L2 `doc`, and sketch
`ui-egui::icons::paint_icon` and `Tokens` at L5, model presets in L2 `doc`, and sketch
fonts at L1. State, projection, interaction, commands and GPU bindings stay in
their current modules. Embedded data uses `CARGO_MANIFEST_DIR`; application code
needs no runtime resource registry or repository lookup.
+11
View File
@@ -0,0 +1,11 @@
//! Original illustrative opaque finishes; not measured optical or engineering data.
//! (name, sRGB color, metal fraction, perceptual roughness). IOR is an illustrative 1.5.
//! Separate names preserve the scalar library's appearance and saved assignments.
pub const OPAQUE_FINISHES: [(&str, [u8; 3], f64, f64); 6] = [
("Steel - Satin PBR", [158, 164, 172], 1.0, 0.4),
("Aluminum - Satin PBR", [204, 208, 214], 1.0, 0.3),
("Copper - Clean PBR", [204, 122, 80], 1.0, 0.25),
("ABS - Molded PBR", [222, 222, 216], 0.0, 0.4),
("PLA - Smooth PBR", [110, 160, 222], 0.0, 0.35),
("Wood - Oak Uniform PBR Preview", [190, 146, 92], 0.0, 0.65),
];
+292
View File
@@ -0,0 +1,292 @@
//! Original starter visual data, MIT OR Apache-2.0. No external material library.
use crate::appearance_pack::{Category, GlassBoundary, GlassRecipe, OpaqueRecipe, TextureAxis, TextureRecipe, VisualPreset};
const WOOD: &str = "Uniform opaque preview only; grain resources/mapping are not applied";
const SOLAR: &str = "Uniform opaque preview only; no cell grid, busbars, electricity or efficiency claim";
const REGOLITH: &str = "Uniform opaque preview only; no granular displacement or measured lunar optical data";
const OPAQUE: &str = "Illustrative opaque isotropic PBR; no measured alloy, polymer or engineering properties";
const fn look(color: [u8; 3], metallic: f64, roughness: f64, dielectric_ior: f64) -> OpaqueRecipe {
OpaqueRecipe { color, metallic, roughness, dielectric_ior }
}
const fn maps(id: &'static str, repeat_mm: [f64; 2], grain: bool) -> Option<TextureRecipe> {
Some(TextureRecipe { id, repeat_mm, grain_axis: if grain { Some(TextureAxis::V) } else { None } })
}
const fn glass(transmission_linear_rgb: [f64; 3], ior: f64, reference_path_mm: f64, boundary: GlassBoundary) -> GlassRecipe {
GlassRecipe { transmission_linear_rgb, dielectric_ior: ior, reference_path_mm, boundary }
}
pub const STARTER_PACK: [VisualPreset; 40] = [
VisualPreset::opaque(
"starter.wood.oak-honey",
"Wood - Honey Oak Uniform Preview",
Category::Wood,
look([190, 145, 88], 0.0, 0.64, 1.5),
WOOD,
maps("starter.wood.oak-honey.maps-v1", [120.0, 240.0], true),
),
VisualPreset::opaque(
"starter.wood.oak-smoked",
"Wood - Smoked Oak Uniform Preview",
Category::Wood,
look([91, 72, 55], 0.0, 0.68, 1.5),
WOOD,
maps("starter.wood.oak-smoked.maps-v1", [120.0, 240.0], true),
),
VisualPreset::opaque(
"starter.wood.walnut-dark",
"Wood - Dark Walnut Uniform Preview",
Category::Wood,
look([99, 66, 43], 0.0, 0.60, 1.5),
WOOD,
maps("starter.wood.walnut-dark.maps-v1", [100.0, 220.0], true),
),
VisualPreset::opaque(
"starter.wood.maple-light",
"Wood - Light Maple Uniform Preview",
Category::Wood,
look([221, 192, 141], 0.0, 0.57, 1.5),
WOOD,
maps("starter.wood.maple-light.maps-v1", [100.0, 200.0], true),
),
VisualPreset::opaque(
"starter.wood.cherry-warm",
"Wood - Warm Cherry Uniform Preview",
Category::Wood,
look([164, 99, 65], 0.0, 0.58, 1.5),
WOOD,
maps("starter.wood.cherry-warm.maps-v1", [110.0, 220.0], true),
),
VisualPreset::opaque(
"starter.wood.ash-pale",
"Wood - Pale Ash Uniform Preview",
Category::Wood,
look([209, 186, 146], 0.0, 0.70, 1.5),
WOOD,
maps("starter.wood.ash-pale.maps-v1", [120.0, 260.0], true),
),
VisualPreset::opaque(
"starter.wood.birch-cream",
"Wood - Cream Birch Uniform Preview",
Category::Wood,
look([228, 206, 163], 0.0, 0.63, 1.5),
WOOD,
maps("starter.wood.birch-cream.maps-v1", [100.0, 200.0], true),
),
VisualPreset::opaque(
"starter.wood.teak-golden",
"Wood - Golden Teak Uniform Preview",
Category::Wood,
look([167, 122, 66], 0.0, 0.61, 1.5),
WOOD,
maps("starter.wood.teak-golden.maps-v1", [110.0, 250.0], true),
),
VisualPreset::opaque(
"starter.metal.carbon-steel-polished",
"Metal - Polished Carbon Steel",
Category::Metal,
look([161, 166, 173], 1.0, 0.16, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.stainless-soft-satin",
"Metal - Soft Satin Stainless",
Category::Metal,
look([187, 193, 201], 1.0, 0.36, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.aluminum-bead-blast",
"Metal - Bead Blast Aluminum",
Category::Metal,
look([195, 201, 210], 1.0, 0.58, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.aluminum-silver-polish",
"Metal - Silver Polish Aluminum",
Category::Metal,
look([218, 224, 232], 1.0, 0.13, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.copper-aged-warm",
"Metal - Warm Aged Copper",
Category::Metal,
look([171, 95, 57], 1.0, 0.48, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.brass-champagne",
"Metal - Champagne Satin Brass",
Category::Metal,
look([205, 173, 102], 1.0, 0.32, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.bronze-dark-cast",
"Metal - Dark Cast Bronze",
Category::Metal,
look([133, 95, 58], 1.0, 0.66, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.titanium-matte",
"Metal - Matte Gray Titanium",
Category::Metal,
look([147, 149, 157], 1.0, 0.53, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.gold-warm",
"Metal - Warm Reflective Gold",
Category::Metal,
look([235, 190, 82], 1.0, 0.18, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.metal.silver-cool",
"Metal - Cool Reflective Silver",
Category::Metal,
look([225, 230, 237], 1.0, 0.12, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.abs-charcoal",
"Plastic - Charcoal ABS",
Category::Plastic,
look([43, 45, 48], 0.0, 0.44, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.abs-warm-white",
"Plastic - Warm White ABS",
Category::Plastic,
look([232, 229, 220], 0.0, 0.40, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque("starter.plastic.pla-coral", "Plastic - Coral PLA", Category::Plastic, look([218, 99, 83], 0.0, 0.43, 1.5), OPAQUE, None),
VisualPreset::opaque(
"starter.plastic.petg-cyan-opaque",
"Plastic - Opaque Cyan PETG",
Category::Plastic,
look([36, 148, 167], 0.0, 0.29, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.nylon-ivory",
"Plastic - Ivory Nylon",
Category::Plastic,
look([220, 213, 190], 0.0, 0.62, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.pc-graphite-opaque",
"Plastic - Opaque Graphite PC",
Category::Plastic,
look([70, 76, 84], 0.0, 0.27, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.hdpe-natural-opaque",
"Plastic - Opaque Natural HDPE",
Category::Plastic,
look([221, 222, 207], 0.0, 0.48, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque("starter.plastic.pom-black", "Plastic - Black POM", Category::Plastic, look([29, 31, 35], 0.0, 0.32, 1.5), OPAQUE, None),
VisualPreset::opaque(
"starter.plastic.tpu-matte-black",
"Plastic - Matte Black TPU",
Category::Plastic,
look([31, 30, 32], 0.0, 0.83, 1.5),
OPAQUE,
None,
),
VisualPreset::opaque(
"starter.plastic.peek-amber-opaque",
"Plastic - Opaque Amber PEEK",
Category::Plastic,
look([157, 119, 62], 0.0, 0.51, 1.5),
OPAQUE,
None,
),
VisualPreset::glass(
"starter.glass.clear-soda-lime",
"Glass Recipe - Clear Soda Lime",
glass([0.92, 0.98, 0.96], 1.52, 3.0, GlassBoundary::Closed),
),
VisualPreset::glass("starter.glass.low-iron-clear", "Glass Recipe - Low Iron Clear", glass([0.98, 0.99, 1.0], 1.50, 4.0, GlassBoundary::Closed)),
VisualPreset::glass("starter.glass.smoky-neutral", "Glass Recipe - Smoky Neutral", glass([0.35, 0.38, 0.42], 1.52, 3.0, GlassBoundary::Closed)),
VisualPreset::glass("starter.glass.amber-tint", "Glass Recipe - Amber Tint", glass([0.84, 0.38, 0.08], 1.51, 3.0, GlassBoundary::Closed)),
VisualPreset::glass("starter.glass.cobalt-blue", "Glass Recipe - Cobalt Blue", glass([0.06, 0.14, 0.76], 1.53, 3.0, GlassBoundary::Closed)),
VisualPreset::glass(
"starter.glass.thin-clear",
"Glass Recipe - Thin Clear Sheet",
glass([0.95, 0.98, 0.99], 1.50, 2.0, GlassBoundary::Thin { thickness_mm: 2.0 }),
),
VisualPreset::opaque(
"starter.solar.mono-blue",
"Solar Panel - Mono Blue Uniform Preview",
Category::Solar,
look([20, 41, 85], 0.0, 0.20, 1.5),
SOLAR,
maps("starter.solar.mono-blue.maps-v1", [160.0, 160.0], false),
),
VisualPreset::opaque(
"starter.solar.thin-film-dark",
"Solar Panel - Dark Thin Film Uniform Preview",
Category::Solar,
look([28, 33, 42], 0.0, 0.28, 1.5),
SOLAR,
maps("starter.solar.thin-film-dark.maps-v1", [100.0, 200.0], false),
),
VisualPreset::opaque(
"starter.regolith.mare-charcoal",
"Regolith - Mare Charcoal Uniform Preview",
Category::Regolith,
look([56, 56, 55], 0.0, 0.97, 1.5),
REGOLITH,
maps("starter.regolith.mare-charcoal.maps-v1", [200.0, 200.0], false),
),
VisualPreset::opaque(
"starter.regolith.highland-pale",
"Regolith - Highland Pale Uniform Preview",
Category::Regolith,
look([116, 113, 106], 0.0, 0.93, 1.5),
REGOLITH,
maps("starter.regolith.highland-pale.maps-v1", [200.0, 200.0], false),
),
VisualPreset::opaque(
"starter.regolith.dust-neutral",
"Regolith - Neutral Dust Uniform Preview",
Category::Regolith,
look([86, 85, 81], 0.0, 1.0, 1.5),
REGOLITH,
maps("starter.regolith.dust-neutral.maps-v1", [150.0, 150.0], false),
),
VisualPreset::opaque(
"starter.regolith.agglutinate-dark",
"Regolith - Dark Agglutinate Uniform Preview",
Category::Regolith,
look([66, 63, 60], 0.0, 0.89, 1.5),
REGOLITH,
maps("starter.regolith.agglutinate-dark.maps-v1", [200.0, 200.0], false),
),
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+87 -66
View File
@@ -1,88 +1,93 @@
//! Original SolidCraft mark, shared by desktop, packaging and CLI.
//! Original SolidCraft mark, shared by desktop, packaging and CLI: the borderless mark, five
//! faces with transparent gaps between them, drawn from the macOS icon's
//! `macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`.
type P = (f32, f32);
/// One face in that SVG's units: its outline, and its linear gradient from one point and
/// colour to another.
struct Face {
outline: &'static [P],
from: (P, [u8; 3]),
to: (P, [u8; 3]),
}
const FACES: [Face; 5] = [
// Inner side of the upper step.
Face {
outline: &[(547.883, 282.66), (709.797, 188.211), (709.797, 375.985)],
from: ((619.14, 179.734), [29, 99, 198]),
to: ((694.687, 396.761), [21, 73, 158]),
},
// Top of the lower step.
Face {
outline: &[(727.797, 593.626), (727.797, 407.136), (889.571, 500.381)],
from: ((619.14, 413.244), [69, 159, 255]),
to: ((790.839, 605.547), [35, 121, 231]),
},
// Right face, with the recess.
Face {
outline: &[(530.0, 521.17), (691.797, 427.912), (691.797, 655.928), (907.594, 531.545), (907.594, 740.159), (530.0, 957.8)],
from: ((632.876, 399.508), [27, 102, 207]),
to: ((783.971, 968.175), [18, 69, 150]),
},
// Left face, stepped.
Face {
outline: &[(116.406, 510.769), (116.406, 303.529), (494.0, 521.17), (494.0, 957.8), (314.203, 854.167), (314.203, 624.777)],
from: ((207.063, 303.357), [37, 141, 245]),
to: ((426.837, 968.175), [20, 97, 218]),
},
// Top of the extruded profile.
Face {
outline: &[(134.374, 272.334), (512.0, 53.364), (691.828, 157.015), (476.117, 282.846), (673.752, 396.761), (512.0, 489.993)],
from: ((385.63, 53.364), [81, 173, 255]),
to: ((522.989, 510.769), [43, 131, 241]),
},
];
/// The mark's extent in SVG units: left, top, width, height.
const BOUNDS: (f32, f32, f32, f32) = (116.406, 53.364, 791.188, 904.436);
/// This block is the icon image data
pub fn rgba(size: usize) -> Result<Vec<u8>, String> {
if !(1..=2048).contains(&size) {
return Err("icon size must be 1..=2048".into());
}
type P = (f32, f32);
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
let ss = if size > 512 { 2 } else { 4 };
let n = size * ss;
let mut buf = vec![[0u8; 4]; n * n];
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
// Fill a convex polygon given in SVG units.
let mut fill = |pts: &[P], col: [u8; 3]| {
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
let (bx, by, bw, bh) = BOUNDS;
let s = n as f32 / bh; // SVG units -> samples: the mark fills the height
let (ox, oy) = ((n as f32 - bw * s) / 2.0 - bx * s, -by * s); // and is centred sideways
// Fill each face's outline (even-odd spans per row, so concave outlines work) with its gradient.
for face in &FACES {
let v: Vec<P> = face.outline.iter().map(|p| (ox + p.0 * s, oy + p.1 * s)).collect();
let (((fx, fy), c0), ((tx, ty), c1)) = (face.from, face.to);
let (gx, gy) = (tx - fx, ty - fy);
let g2 = gx * gx + gy * gy;
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
let mut xs = Vec::with_capacity(v.len());
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
let cy = y as f32 + 0.5;
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
xs.clear();
for i in 0..v.len() {
let (a, b) = (v[i], v[(i + 1) % v.len()]);
if (a.1 <= cy) != (b.1 <= cy) {
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
lo = lo.min(x);
hi = hi.max(x);
xs.push(a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0));
}
}
if lo > hi {
continue;
}
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
xs.sort_by(f32::total_cmp);
for &[from, to] in xs.as_chunks::<2>().0 {
for x in ((from - 0.5).ceil() as i32).max(0)..=((to - 0.5).floor() as i32).min(n as i32 - 1) {
// The sample's place along the gradient, in SVG units.
let (px, py) = ((x as f32 + 0.5 - ox) / s, (cy - oy) / s);
let t = (((px - fx) * gx + (py - fy) * gy) / g2).clamp(0.0, 1.0);
let mix = |k: usize| (f32::from(c0[k]) + (f32::from(c1[k]) - f32::from(c0[k])) * t).round() as u8;
buf[y as usize * n + x as usize] = [mix(0), mix(1), mix(2), 255];
}
}
}
};
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
let white = [255, 255, 255];
let mut stroke = |pts: &[P], closed: bool| {
let m = pts.len();
for i in 0..(if closed { m } else { m - 1 }) {
let (a, b) = (pts[i], pts[(i + 1) % m]);
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
}
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
let disc: Vec<P> = (0..16)
.map(|k| {
let t = k as f32 * std::f32::consts::TAU / 16.0;
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
})
.collect();
fill(&disc, white);
}
};
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
stroke(
&[
(178.5, 5.8),
(5.0, 105.9),
(5.0, 206.1),
(91.7, 256.2),
(91.7, 356.4),
(178.5, 406.4),
(352.0, 306.3),
(352.0, 206.1),
(265.2, 156.0),
(265.2, 55.9),
],
true,
);
}
// Average each ss x ss block down to one pixel (straight alpha = covered share).
let mut rgba = Vec::with_capacity(size * size * 4);
for y in 0..size {
@@ -123,4 +128,20 @@ mod tests {
assert!(super::rgba(0).is_err());
assert!(super::rgba(2049).is_err());
}
/// Borderless: no white edge lines, and the gaps between faces are see-through. At 256 px
/// the gap between the left and right faces (SVG x 494..530) is transparent on the row
/// through the middle of the mark, and no opaque pixel is white.
#[test]
fn shared_mark_is_borderless() {
let size = 256;
let pixels = super::rgba(size).expect("valid size");
let px = |x: usize, y: usize| pixels.as_chunks::<4>().0[y * size + x];
let s = size as f32 / 904.436;
let gap_x = ((size as f32 - 791.188 * s) / 2.0 + (512.0 - 116.406) * s) as usize;
let row = ((750.0 - 53.364) * s) as usize;
assert_eq!(px(gap_x, row)[3], 0, "the seam between the left and right faces is a gap");
assert!(px(gap_x - 8, row)[3] == 255 && px(gap_x + 8, row)[3] == 255, "faces on both sides of the gap");
assert!(pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 0 || p[0] < 200), "no white edges");
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 44 KiB

+1 -1
View File
@@ -91,7 +91,7 @@ Palette changes preserve accepted artwork and never update review hashes.
| `source/` | Rust viewport glyphs, markers, navigation and pointer; not command icons |
`crates/ui-egui/build.rs` embeds SVGs, palette and aliases. `icon_svg.rs` parses and
rasterises them for `icons::paint`; adding an icon still requires its registry or
rasterises them for `icons::paint_icon`; adding an icon still requires its registry or
UI assignment and an application rebuild. Review status never controls rendering.
## Checks and reports
+3 -3
View File
@@ -78,7 +78,7 @@ Its projected circle uses the same axes; do not arbitrarily flatten the ellipse.
Only use the stable `source` identifiers below in model SVGs. The renderer maps roles to
light/dark theme values. `currentColor` is the background line-art role, not a way
to add arbitrary colours. All values and accents come from `design/palette.toml`.
to add arbitrary colours. Default display values and semantic accents come from `design/palette.toml`. Theme files may override those display roles and ramps through `icon_palette`; source identifiers and accent assignments stay unchanged.
| Role | Author helper | Light value | Use |
| --- | --- | --- | --- |
@@ -96,9 +96,9 @@ The source hexes identify paint roles, not the displayed colour. **Never recolou
SVG to apply a workspace accent.** Keep these identifiers and every finished shape
unchanged; the runtime and review page resolve them through `design/palette.toml`.
That file is the single colour authority: edit `light`/`dark` in the relevant
That file is the default colour authority: edit `light`/`dark` in the relevant
`accents` ramp; `active-3` is its primary stroke and `active-1/2` are shaded faces.
Do not edit a role's `source` identifier. `variants` holds colour-vision adjustments.
Do not edit a role's `source` identifier. Theme `icon_palette` overrides affect display colours only; colour-vision variants still take precedence. `variants` holds colour-vision adjustments.
A role with `use = "active-3"` follows that stroke automatically.
| Meaning | Accent | Assignment |
+1
View File
@@ -29,6 +29,7 @@ ground = "c_fix"
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
sketch_break = { master = "break", accent = "sketch" }
sketch_cancel = { master = "close", accent = "sketch" }
sketch_dxf = { master = "dxf", accent = "sketch" }
sketch_move = { master = "move", accent = "sketch" }
sketch_scale = { master = "scale", accent = "sketch" }
@@ -104,6 +104,7 @@ flange false
flat false
fold false
folder false
frame_member false
gear false
graph false
ground false
@@ -147,6 +148,7 @@ origin false
pan false
params false
paste false
path_flange false
pattern_circ false
pattern_path false
pattern_rect false
@@ -203,12 +205,15 @@ scale false
screw false
section false
select false
send false
settings false
sheet_rules false
shell false
shield false
silhouette false
sketch false
sketch_break false
sketch_cancel false
sketch_chamfer false
sketch_chamfer_angle false
sketch_chamfer_equal false
@@ -237,6 +242,7 @@ split_face false
spun false
star false
status_error false
status_recovery false
style false
suppress false
surface_blend false
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Send message</title>
<path d="M32 51L32 13M17 28L32 13L47 28" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Keyboard shortcuts</title>
<path d="M13 16L51 16A5 5 0 0 1 56 21L56 43A5 5 0 0 1 51 48L13 48A5 5 0 0 1 8 43L8 21A5 5 0 0 1 13 16Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="20" cy="27" r="2.8" fill="currentColor"/>
<circle cx="32" cy="27" r="2.8" fill="currentColor"/>
<circle cx="44" cy="27" r="2.8" fill="currentColor"/>
<path d="M20 39L44 39" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 607 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Graphics display</title>
<path d="M13 10L51 10A5 5 0 0 1 56 15L56 37A5 5 0 0 1 51 42L13 42A5 5 0 0 1 8 37L8 15A5 5 0 0 1 13 10ZM32 42L32 54M24 54L40 54" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Mouse navigation</title>
<path d="M32 8A13 13 0 0 1 45 21L45 43A13 13 0 0 1 19 43L19 21A13 13 0 0 1 32 8ZM32 8L32 24" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Interface palette</title>
<path d="M32 8C18.75 8 8 18.75 8 32C8 45.25 18.75 56 32 56C37 56 39 52.5 36.5 49C34 45.5 36 42 40 42L45 42C51.1 42 56 37.1 56 31C56 19 45 8 32 8Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="21" cy="29" r="2.8" fill="currentColor"/>
<circle cx="29" cy="20" r="2.8" fill="currentColor"/>
<circle cx="41" cy="23" r="2.8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Plugins</title>
<path d="M24 8L24 21M40 8L40 21M16 21L48 21L48 29A16 16 0 0 1 16 29L16 21ZM32 45L32 56" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Approval required</title>
<path d="M32 9L51 16L51 31C51 43 43 51 32 56C21 51 13 43 13 31L13 16Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Stop response</title>
<path d="M20 17L44 17A3 3 0 0 1 47 20L47 44A3 3 0 0 1 44 47L20 47A3 3 0 0 1 17 44L17 20A3 3 0 0 1 20 17Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Frame Member</title>
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="9.25" cy="18.86" r="3" fill="#236ead"/>
<path d="M11.88 20.39L40.62 36.98L40.62 50.25L11.88 33.66Z" fill="#328bd3"/>
<path d="M23.38 13.75L52.12 30.34L40.62 36.98L11.88 20.39Z" fill="#b9ddf8"/>
<path d="M52.12 30.34L40.62 36.98L40.62 50.25L52.12 43.61ZM49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#8e9faf" fill-rule="evenodd"/>
<defs><clipPath id="fm_hole"><path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none"/></clipPath></defs>
<g clip-path="url(#fm_hole)">
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#236ead"/>
<path d="M49.8 34.35L49.8 42.28L42.86 38.27L42.86 30.34Z" fill="#328bd3"/>
<path d="M49.8 42.28L42.94 46.24L35.99 42.23L42.86 38.27Z" fill="#b9ddf8"/>
</g>
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23.38 13.75L52.12 30.34L52.12 43.61L40.62 50.25L11.88 33.66L11.88 20.39Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M11.88 20.39L40.62 36.98L52.12 30.34" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M40.62 36.98L40.62 50.25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="54.75" cy="45.14" r="3" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Path Flange</title>
<path d="M9.5 44L23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#34465b" stroke-width="11.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.5 44L23.5 44" fill="none" stroke="#b8c3cf" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#328bd3" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Recover unsaved design</title>
<circle cx="32" cy="33" r="22" fill="#b9ddf8"/>
<path d="M10 33A22 22 0 1 0 16.44 17.44L10 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L10 24L22 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 21L32 33L41 39" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

+15 -3
View File
@@ -99,14 +99,23 @@ pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
let s = 20.0 / 412.217;
brand_mark_styled(p, c, 20.0, crate::theme::Tokens::get().logo, true);
}
/// The same original mark as a single quiet silhouette, without face seams or outlines.
pub(crate) fn brand_mark_silhouette(p: &egui::Painter, c: Pos2, height: f32, ink: Color32) {
brand_mark_styled(p, c, height, [ink; 3], false);
}
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 3], edges: bool) {
let s = height / 412.217;
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
let face = |col: Color32, pts: &[(f32, f32)]| {
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
};
// The faces are separated by gaps that show the application bar, not by white lines.
let [light, mid, dark, _] = crate::theme::Tokens::get().logo;
let [light, mid, dark] = colours;
let edge_col = crate::theme::Tokens::get().app_bar;
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
@@ -117,7 +126,10 @@ pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
// White edge lines.
if !edges {
return;
}
// The seams between the faces and around the mark, in the bar's colour, so they read as gaps.
let edge = Stroke::new(10.0 * s, edge_col);
let line = |pts: &[(f32, f32)]| {
p.add(egui::Shape::line(m(pts), edge));
+36 -3
View File
@@ -1,4 +1,4 @@
//! Constraint, assembly snap and dimension marker artwork.
//! Constraint, assembly snap, hole placement and dimension marker artwork.
use crate::dialogs_assembly::SnapKind;
use crate::theme::Tokens;
use egui::{Color32, Pos2, Stroke, vec2};
@@ -57,6 +57,39 @@ pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: Sn
painter.galley(at, g, t.text);
}
}
/// A place a hole can snap to, before the cursor reaches it: a small hollow mark of its kind
/// (a square for a face's centre, a ring for a round edge's centre).
pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: Color32) {
let ic = Tokens::get().icons;
for stroke in [Stroke::new(3.5, ic.marker_halo), Stroke::new(1.4, col)] {
match kind {
SnapKind::FaceCenter => {
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(8.0, 8.0)), 1.0, stroke, egui::StrokeKind::Middle);
}
SnapKind::CircleCenter => {
painter.circle_stroke(c, 4.5, stroke);
}
SnapKind::EdgeMid | SnapKind::Vertex => {
let r = 5.0;
let diamond = vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)];
painter.add(egui::Shape::convex_polygon(diamond, Color32::TRANSPARENT, stroke));
}
}
}
painter.circle_filled(c, 1.3, col);
}
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
/// while it is hovered or dragged.
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
let ic = Tokens::get().icons;
let r = 6.0;
painter.circle_filled(c, r + 3.5, ic.marker_halo);
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
}
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
Some(match name {
"Coincident" => "c_coincident",
@@ -75,8 +108,8 @@ pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
}
pub(crate) fn glyph_letter(name: &str) -> &'static str {
match name {
"Horizontal" => "H",
"Vertical" => "V",
"Horizontal" => "-",
"Vertical" => "|",
_ => "?",
}
}
+347 -37
View File
@@ -72,6 +72,7 @@
"Choose…": "Vybrat…",
"Circular": "Kruhově",
"Circumference": "Obvod",
"Clockwise": "Ve směru hodinových ručiček",
"Close draft": "Zavřít koncept",
"Collisions": "Kolize",
"Colour": "Barva",
@@ -167,6 +168,7 @@
"HISTORY": "HISTORIE",
"Height": "Výška",
"Hidden edges": "Skryté hrany",
"History layout": "Rozložení historie",
"Holes": "Otvory",
"Holes (edges on them)": "Otvory (jejich hrany)",
"Home view": "Výchozí pohled",
@@ -232,7 +234,7 @@
"No crash reports.": "Žádná hlášení o pádech.",
"No matching appearance presets.": "Žádné odpovídající předvolby vzhledu.",
"No matching appearances": "Žádné odpovídající vzhledy",
"No visible bodies to export.": "Žádná viditelná tělesa k exportu.",
"No visible bodies or sketches to export.": "Žádná viditelná tělesa ani náčrty k exportu.",
"None": "Žádné",
"Not available yet": "Zatím není k dispozici",
"Notch": "Zářez",
@@ -321,6 +323,7 @@
"Search commands": "Hledat příkazy",
"Search this library": "Prohledat tuto knihovnu",
"Section": "Řez",
"Section position": "Poloha průřezu",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vybrat jiné: podržte levé tlačítko na modelu (nebo pravé tlačítko › Vybrat jiné…) a zobrazí se vše pod kurzorem; dalším kliknutím na stejné místo vyberete další prvek. Dvojklikem prostředním tlačítkem přejdete na výchozí pohled (Předvolby › Navigace).",
"Select bodies (a face picks its body)": "Vyberte tělesa (plocha vybere své těleso)",
"Select bodies or faces instead": "Vyberte místo toho tělesa nebo plochy",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Zobrazit kurzor agenta",
"Show in folder": "Zobrazit ve složce",
"Show the pointer an AI agent moves (#35)": "Zobrazit ukazatel, kterým pohybuje AI agent (#35)",
"Show tool at cursor": "Zobrazit nástroj u kurzoru",
"Signed axes": "Osy se znaménkem",
"Size": "Velikost",
"Sketch constraints": "Vazby náčrtu",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se zavřel dříve, než se tyto návrhy uložily. Lze otevřít jejich poslední automatické uložení.",
"Specialise": "Specializovat",
"Speed": "Rychlost",
"Square": "Čtverec",
"Stand it up": "Postavit",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postavit: otočení o čtvrt otáčky kolem osy X jako jeden prvek Posunutí, který lze vrátit.",
"Stand this part up?": "Postavit tento díl?",
"Start angle": "Počáteční úhel",
"Start bulge": "Vypouklost začátku",
"Start continuity": "Návaznost začátku",
"Step": "Krok",
@@ -411,6 +417,7 @@
"angle, height": "úhel, výška",
"appearance.dielectric_ior": "Index lomu dielektrika",
"appearance.metallic": "Kovovost",
"appearance.only_available": "Pouze dostupné vzhledy",
"appearance.pbr_help": "Tento povrch zobrazíte v renderu nebo v produktovém PNG. Bez lomu a průchodu světla.",
"appearance.pbr_opaque": "Neprůhledné PBR",
"appearance.roughness": "Drsnost",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Vložit STEP",
"cmd.file_save_mesh": "Uložit jako síť",
"cmd.file_slicers": "Nainstalované slicery",
"cmd.frame_member": "Prvek rámu",
"cmd.frame_profile_insert": "Vložit profil",
"cmd.frame_profile_list": "Knihovna profilů",
"cmd.frame_profile_properties": "Vlastnosti profilu",
"cmd.inspect_accessibility": "Analýza přístupnosti",
"cmd.inspect_center_of_mass": "Těžiště",
"cmd.inspect_clearance": "Vůle",
@@ -568,6 +579,7 @@
"cmd.material_create": "Nový fyzický materiál",
"cmd.material_delete": "Smazat fyzický materiál",
"cmd.material_edit": "Upravit fyzický materiál",
"cmd.material_inspect": "Zkontrolovat fyzický materiál",
"cmd.material_list": "Seznam materiálů",
"cmd.material_properties_directional": "Směrový modul pružnosti",
"cmd.material_properties_validate": "Ověřit data fyzického materiálu",
@@ -614,6 +626,8 @@
"cmd.plastic_rules": "Seznam pravidel pro plasty",
"cmd.plastic_snap_fit": "Zacvakávací spoj",
"cmd.render_backends": "Renderovací jádra",
"cmd.render_camera_fit": "Zarámovat objekt renderu",
"cmd.render_camera_get": "Prohlédnout kameru renderu",
"cmd.render_cancel": "Zrušit render",
"cmd.render_export": "Exportovat obrázek renderu",
"cmd.render_preflight": "Zkontrolovat požadavek na render",
@@ -621,6 +635,7 @@
"cmd.render_settings_apply": "Použít kameru renderu",
"cmd.render_settings_get": "Nastavení renderu",
"cmd.render_settings_set": "Uložit nastavení renderu",
"cmd.render_stage_backdrop": "Uložit fyzické pozadí renderu",
"cmd.render_start": "Renderovat obrázek",
"cmd.render_status": "Průběh renderu",
"cmd.select_clear": "Zrušit výběr",
@@ -831,7 +846,7 @@
"dxf.progress.projection": "Promítání geometrie (3/5)",
"dxf.progress.write": "Zápis DXF (5/5)",
"dxf.tolerance": "Tolerance křivek",
"dxf.tolerance_help": "Pouze viditelná tělesa. Křivky jsou nahrazeny úsečkami. Menší tolerance dává jemnější křivky, ale zabere více času a paměti.",
"dxf.tolerance_help": "Pouze viditelná tělesa a zobrazené náčrty. Křivky jsou nahrazeny úsečkami. Menší tolerance dává jemnější křivky, ale zabere více času a paměti.",
"file_load.built": "Dosud sestaveno: {built} z {total} prvků.",
"file_load.converting": "Převod tělesa {done} z {total}",
"file_load.inserting": "Vkládání geometrie",
@@ -856,19 +871,57 @@
"help.mit": "Licence MIT",
"help.notice": "NOTICE",
"help.third_party": "Licence třetích stran",
"home.back": "Zpět na {name}",
"home.crashes": "Hlášení o pádech",
"home.design_workspace": "Návrh",
"home.documentation": "Dokumentace",
"home.export": "Exportovat",
"home.export_hint": "Vyberte formát pro aktuální návrh.",
"home.export_json_hint": "Upravitelný návrh a historie příkazů",
"home.export_mesh_hint": "Síť pro tisk nebo sdílení",
"home.export_step_hint": "Přesná geometrie tělesa",
"home.import": "Importovat…",
"home.import_hint": "STEP, IGES nebo síť do nového návrhu",
"home.in_this_browser": "v tomto prohlížeči",
"home.missing": "chybí",
"home.month_01": "led",
"home.month_02": "úno",
"home.month_03": "bře",
"home.month_04": "dub",
"home.month_05": "kvě",
"home.month_06": "čer",
"home.month_07": "čvc",
"home.month_08": "srp",
"home.month_09": "zář",
"home.month_10": "říj",
"home.month_11": "lis",
"home.month_12": "pro",
"home.new_inches": "Nový návrh v palcích",
"home.new_inches_hint": "Palce · otevře se v pracovním prostoru Návrh",
"home.new_mm": "Nový návrh",
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
"home.open": "Otevřít…",
"home.opened_days_ago": "Otevřeno před {count} dny",
"home.opened_hours_ago": "Otevřeno před {count} h",
"home.opened_just_now": "Otevřeno právě teď",
"home.opened_minutes_ago": "Otevřeno před {count} min",
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
"home.open_now": "Otevřít nyní",
"home.opened_days_ago": "Před {count} dny",
"home.opened_hours_ago": "Před {count} h",
"home.opened_just_now": "Právě teď",
"home.opened_minutes_ago": "Před {count} min",
"home.opening": "otevírání…",
"home.properties": "Vlastnosti návrhu",
"home.recent": "Nedávné",
"home.recent_empty": "Zde se zobrazí návrhy, které otevřete nebo uložíte.",
"home.recovery_count": "Návrhy k obnovení: {count}",
"home.recovery_empty": "Žádné návrhy k obnovení.",
"home.recovery_hint": "SolidCraft se zavřel před uložením těchto návrhů.",
"home.report_issue": "Nahlásit problém",
"home.samples": "Ukázky",
"home.settings": "Nastavení",
"home.short_date": "{day}. {month}",
"home.shortcuts": "Klávesové zkratky",
"home.start_hint": "Vytvořte návrh, otevřete nedávný soubor nebo prozkoumejte ukázku.",
"home.whats_new": "Co je nového",
"home.yesterday": "Včera",
"install.button_tip": "Nainstalovat SolidCraft jako aplikaci: otevře se ve vlastním okně a funguje i offline",
"install.dock_1": "Zvolte Soubor › Přidat do Docku.",
"install.dock_2": "Klikněte na Přidat.",
@@ -946,6 +999,16 @@
"pattern.direction_1": "Směr 1",
"pattern.direction_2": "Směr 2",
"pattern.flip_direction": "Převrátit směr",
"perf.click_for_details": "Klikněte pro podrobnosti: graf časů snímků, histogram a co vykreslila GPU",
"perf.draw_calls": "Volání vykreslení",
"perf.line_segments": "Úsečky",
"perf.low_1": "1% minimum",
"perf.renderer": "Vykreslování",
"perf.scene_build": "Sestavení scény",
"perf.triangles": "Trojúhelníky",
"perf.ui_time": "Čas UI",
"perf.viewport": "Pohled",
"perf.worst": "nejhorší",
"pick a hole to seat it, or OK places it at the origin": "vyberte otvor pro usazení, nebo OK umístí do počátku",
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
"prefs.cpu": "CPU (softwarově)",
@@ -958,20 +1021,51 @@
"recovery.min_ago": "před {count} min",
"recovery.never_saved": "nikdy neuloženo",
"render.background": "Pozadí scény",
"render.bloom_enable": "Záře ve výstupu",
"render.bloom_help": "Kreativní záře z lineárních jasů se třemi normalizovanými průchody. Poloměr v pixelech výstupu, přičtení před sRGB. Nejde o simulaci čočky, objemu ani zlepšení konvergence. Surové EXR se nemění.",
"render.bloom_opaque": "Záře vyžaduje neprůhledný výstup. Průhledné PNG se odmítne, neprovede se skryté sloučení.",
"render.bloom_radius": "Poloměr záře",
"render.bloom_strength": "Síla záře",
"render.bloom_threshold": "Lineární práh jasů",
"render.cpu_aa_help": "Prostorové vzorky: více vzorků zlepší hrany a prodlouží vykreslení.",
"render.cpu_key_rim": "Hlavní a obrysové",
"render.cpu_lighting": "Předvolba osvětlení",
"render.cpu_neutral": "Neutrální",
"render.cpu_opaque_required": "CPU trasování vyžaduje neprůhledné pozadí. Vypněte průhledné pozadí.",
"render.cpu_options_pending": "Panel podporuje vzorky hran, předvolby světel, stíny a expozici. Další volby jsou v API.",
"render.cpu_primary_samples": "Vzorky hran na pixel",
"render.cpu_shadows": "Vrhat stíny",
"render.cpu_summary": "Neprůhledný CPU PNG · světla · stíny · bez skla a textur",
"render.cpu_warm_cool": "Teplé a studené",
"render.exr_pending": "GPU může uchovat surové lineární EXR. Komprese, vícevrstvé obrazy a pomocné průchody zatím nejsou dostupné.",
"render.fit": "Přizpůsobit produktu",
"render.gpu_depth": "Maximální hloubka cesty",
"render.gpu_options_help": "Tyto volby platí pro tuto úlohu. Expozice vychází z uloženého nastavení.",
"render.gpu_path_failed": "Trasování cest GPU není dostupné",
"render.gpu_path_first_setup": "První příprava na této GPU může trvat několik minut; SolidCraft lze mezitím používat. Při dalších spuštěních se použijí zkompilované shadery, pokud je ovladač uchová.",
"render.gpu_path_preparing": "Trasování cest GPU: kompilace shaderů…",
"render.gpu_seed": "Náhodné semeno",
"render.gpu_unavailable": "Trasování cest GPU není na tomto zařízení připraveno.",
"render.image_advanced": "Pokročilé nastavení GPU",
"render.image_cancelled": "Render zrušen",
"render.image_cancelling": "Rušení",
"render.image_completed": "Obrázek připraven k exportu",
"render.image_encoding": "Kódování PNG",
"render.image_export": "Exportovat PNG",
"render.image_export": "Exportovat hotový obrázek",
"render.image_exposure": "Expozice",
"render.image_failed": "Render selhal",
"render.image_height": "Výška",
"render.image_help": "Náhledový obrázek z CPU · PNG · přibližná průhlednost",
"render.image_host_missing": "Není dostupný vykreslovací hostitel.",
"render.image_method": "Metoda obrazu",
"render.image_overwrite": "Nahradit existující soubor",
"render.image_preparing": "Příprava obrázku",
"render.image_progressive": "Progresivní render",
"render.image_queued": "Čeká na render",
"render.image_reduce": "Zkuste menší obrázek nebo jednodušší scénu.",
"render.image_running": "Renderování obrázku",
"render.image_samples": "Vzorků na pixel",
"render.image_scene_stale": "Před kontrolou nebo spuštěním dokončete modelování nebo obnovte scénu.",
"render.image_stale": "Tento obrázek používá scénu zachycenou před pozdějšími změnami.",
"render.image_start": "Spustit render",
"render.image_supported": "Požadavek na render je podporován; fronta a paměť se znovu ověří při spuštění.",
@@ -981,17 +1075,135 @@
"render.image_unknown": "Stav renderu není k dispozici",
"render.image_use_largest": "Použít největší velikost v tomto poměru stran:",
"render.image_width": "Šířka",
"render.material_absorption": "Absorpce R · G · B (na mm)",
"render.material_add": "Přidat nebo upravit těleso",
"render.material_assignments": "Přiřazení těles",
"render.material_binding_enable": "Zahrnout úpravy tohoto tělesa",
"render.material_box": "Osově zarovnaný kvádr",
"render.material_browse": "Vybrat obrázek…",
"render.material_certification": "Uzavřené sklo používá geometrickou vzdálenost. GPU příprava ověřuje topologii, orientaci, překryvy a optická rozhraní; kontrola jen přijímá požadavek.",
"render.material_choose_body": "Vybrat těleso scény",
"render.material_closed": "Uzavřená objemová síť",
"render.material_coat": "Čirý povlak na základním materiálu",
"render.material_coat_enable": "Zapnout čirý povlak",
"render.material_coat_help": "Ilustrativní dielektrický povlak na stávajícím neprůhledném vzhledu. Nulová váha ponechá základ beze změny.",
"render.material_color_map": "Obraz základní barvy (sRGB)",
"render.material_custom": "Vlastní ilustrativní optika",
"render.material_enable": "Použít tyto úpravy materiálů GPU",
"render.material_filter": "Filtrovat tělesa scény…",
"render.material_filter_help": "Zobrazuje se nejvýše 128 shod. U velkých scén zpřesněte filtr.",
"render.material_glass": "Sklo a optická absorpce",
"render.material_glass_enable": "Použít fyzikální sklo na těleso",
"render.material_glass_exclusive": "Tento renderer nepodporuje sklo s povlakem nebo mapami na témže tělese.",
"render.material_glass_help": "Původní ilustrativní recepty. Vzorky ukazují propustnost, ne render. Absorpce je −ln(lineární propustnosti) / referenční vzdálenost; alfa se nemění.",
"render.material_gpu_required": "Aktivní úpravy vyžadují GPU tracing. Před kontrolou nebo spuštěním vyberte GPU nebo úpravy vypněte.",
"render.material_ior": "Index lomu (IOR)",
"render.material_limits": "Nejvýše 128 nastavení těles, 32 oddělených skel, 64 povlaků a 128 vazeb textur. Shodné mapy sdílejí paměť; limity zařízení a celkové paměti zůstávají aktivní.",
"render.material_map_invalid": "Cesty map musí být omezené místní cesty; síla normál musí být konečná mezi 0 a 1.",
"render.material_map_required": "Před použitím map vyberte obraz barvy nebo normál.",
"render.material_maps": "Rovinné mapy barvy a normál",
"render.material_maps_enable": "Zapnout mapy neprůhledného tělesa",
"render.material_maps_help": "PNG/JPEG do 1024×1024; celkem 16 MiB kódovaných dat. U a V jsou kolmé vektory opakování ve světových mm (nejvýše 128 opakování). Barva je sRGB, normály OpenGL +Y. Soubory se zachytí při kontrole/spuštění.",
"render.material_normal_map": "Obraz normál (OpenGL, +Y)",
"render.material_normal_strength": "Síla normál (0–1)",
"render.material_origin": "Počátek mapování X · Y · Z (mm)",
"render.material_refresh": "Obnovit tělesa scény",
"render.material_refresh_needed": "Scéna se změnila. Před přidáním cíle obnovte seznam.",
"render.material_remove": "Odstranit přiřazení",
"render.material_repeat_u": "Jedno opakování U X · Y · Z (mm)",
"render.material_repeat_v": "Jedno opakování V X · Y · Z (mm)",
"render.material_revalidate": "Znovu ověřit těleso",
"render.material_revalidate_needed": "Přiřazení patří ke starší revizi scény. Před renderem znovu ověřte přesnou identitu tělesa.",
"render.material_roughness": "Drsnost povlaku (0,045–1)",
"render.material_shadow": "Propouštět přímé stínové paprsky",
"render.material_target_missing": "Pořadí a název tělesa již nesouhlasí. Odstraňte přiřazení a znovu výslovně vyberte těleso.",
"render.material_thickness": "Tloušťka vrstvy (mm)",
"render.material_thin": "Tenká vrstva",
"render.material_title": "Materiály GPU obrazu",
"render.material_transient": "Dočasné optické úpravy pro tuto kartu. Neukládají se do návrhu, nastavení renderu ani knihovny materiálů.",
"render.material_weight": "Váha povlaku (0–1)",
"render.method_cpu": "Trasování paprsků CPU",
"render.method_gpu": "Trasování cest GPU",
"render.method_raster": "CPU rastr",
"render.navigation_help": "Kamera se ovládá stejně jako v návrhu (Předvolby › Navigace). Uložit nastavení zaznamená aktuální kameru.",
"render.opaque_image_summary": "Náhledové PNG · neprůhledné PBR · smíšená průhlednost",
"render.opaque_preview_help": "Neprůhledné PBR a jednoduché barvy; smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
"render.output_format_status": "PNG je přesný dokončený obraz. Volitelné GPU EXR uchovává lineární FLOAT32 radianci před filtry. Export nikdy nenahrazuje výsledek náhledem nebo jinou úlohou.",
"render.perspective": "Perspektivní kamera",
"render.preview": "Integrovaný náhled",
"render.preview_help": "Rastrové stínování a smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
"render.progressive_image_summary": "Náhledové PNG · neprůhledné PBR · postupné vzorkování",
"render.reload_settings": "Znovu načíst uložené nastavení",
"render.restore_camera": "Obnovit uloženou kameru",
"render.result_actual_help": "Jeden pixel obrázku na jeden pixel obrazovky.",
"render.result_capture": "Zachycená revize",
"render.result_current": "Zachycená revize modelu je aktuální.",
"render.result_empty": "Zatím není hotový obrázek. Zkontrolujte nastavení a spusťte vykreslování.",
"render.result_fit": "Přizpůsobit obrázek",
"render.result_job": "Úloha",
"render.result_navigation": "Posun a přiblížení používají nastavení navigace. Mezerník + tažení posouvá.",
"render.result_open": "Otevřít hotový obrázek na hlavním plátně.",
"render.result_preview": "Živý náhled",
"render.result_retained": "Zobrazuje se poslední hotový obrázek; novější vykreslení jej zatím nenahradilo.",
"render.result_title": "Výsledek vykreslení",
"render.return_design": "Zpět k návrhu",
"render.ribbon_backgrounds": "Pozadí",
"render.ribbon_check": "Kontrola",
"render.ribbon_check_help": "Ověřit přesný aktuální návrh renderu. Bez skrytého přepnutí vykreslovače, zmenšení nebo nahrazení zdrojů.",
"render.ribbon_edit_lights": "Upravit světla",
"render.ribbon_environment_help": "HDR osvětlení, pozadí kamery a fyzické plochy jsou samostatné ovládací prvky s potvrzením nahrazení.",
"render.ribbon_finish_dialog": "Nejprve dokončete dialog souboru nebo potvrzení.",
"render.ribbon_frame": "Zarámovat",
"render.ribbon_hdr_library": "Knihovna HDR",
"render.ribbon_job_active": "Render již běží; před spuštěním dalšího jej výslovně zrušte.",
"render.ribbon_light_rigs": "Sestavy světel",
"render.ribbon_lights_help": "Zkontrolovat světelné sestavy nebo upravit podporovaná světla; aktuální nastavení nahradí až výslovné použití.",
"render.ribbon_materials": "Materiály",
"render.ribbon_materials_help": "Otevřít inspektor vzhledu a původní povrchy. Optické úpravy nemění skrytě technické vlastnosti.",
"render.ribbon_no_active_job": "Není aktivní render ke zrušení.",
"render.ribbon_no_result": "Před exportem dokončete render.",
"render.ribbon_no_saved_camera": "Před obnovením kamery uložte záběr.",
"render.ribbon_saved_camera": "Uložená kamera",
"render.ribbon_shots": "Záběry",
"render.ribbon_studios": "Studia",
"render.ribbon_studios_help": "Procházejte dostupná studia a před použitím zkontrolujte nahrazované prvky. Koncepty zůstávají označené.",
"render.ribbon_surface_maps": "Mapy povrchu",
"render.save_setup": "Uložit nastavení",
"render.setup_name": "Název nastavení",
"render.studio_add_area": "Přidat plošné světlo",
"render.studio_ambient": "Konstantní zář prostředí",
"render.studio_aperture": "Poloměr clony",
"render.studio_area": "Plošné světlo",
"render.studio_area_help": "Nejvýše čtyři jednostranné obdélníky. Kolmé poloviční hrany U a V určují velikost; U × V míří na osvětlenou stranu. Souřadnice jsou světové milimetry.",
"render.studio_center": "Střed X, Y, Z (mm)",
"render.studio_denoise": "Potlačit šum v hotovém obrazu",
"render.studio_denoise_help": "Závěrečný filtr respektuje hrany; může změkčit detaily. Sklo a ostré zrcadlové povrchy se nefiltrují.",
"render.studio_direction": "Směr ke slunci (X, Y, Z)",
"render.studio_environment": "Prostředí",
"render.studio_environment_bounds": "Intenzita prostředí musí být 0–100 a otočení −360° až 360°.",
"render.studio_environment_none": "Bez mapy prostředí",
"render.studio_focus": "Osová zaostřovací vzdálenost",
"render.studio_half_u": "Poloviční hrana U (mm)",
"render.studio_half_v": "Poloviční hrana V (mm)",
"render.studio_hdr": "Soubor Radiance HDR",
"render.studio_hdr_browse": "Vybrat HDR…",
"render.studio_hdr_help": "Radiance .hdr, panorama 2:1, nejvýše 2048 × 1024. EXR není dostupné. Předpoklad barev platí jen při neurčených primárních barvách.",
"render.studio_hdr_required": "Vyberte místní soubor .hdr.",
"render.studio_hdr_srgb": "Neurčené primární barvy chápat jako lineární sRGB",
"render.studio_intensity": "Intenzita prostředí",
"render.studio_lens": "Hloubka ostrosti",
"render.studio_lens_perspective": "Hloubka ostrosti vyžaduje perspektivní kameru.",
"render.studio_lighting": "Studiové osvětlení",
"render.studio_lighting_enable": "Použít vlastní studiové osvětlení",
"render.studio_radiance": "Lineární RGB zář",
"render.studio_remove_area": "Odebrat plošné světlo",
"render.studio_rotation": "Otočení prostředí",
"render.studio_runtime_help": "Optické úpravy těles jsou v Materiálech GPU obrazu. Ovládání studia a materiálů je dočasné a nemění uložené vzhledy.",
"render.studio_sky": "Umělecká obloha",
"render.studio_sky_help": "Původní přechod oblohy pro osvětlení; nejde o kalibrovaný model atmosféry.",
"render.studio_sun": "Slunce",
"render.studio_sun_radius": "Úhlový poloměr",
"render.studio_tone_map": "Mapování tónů",
"render.unavailable_preview": "Toto nastavení vyžaduje nedostupný profil náhledu.",
"render.unsaved": "Neuložené změny náhledu",
"render.use_preview": "Použít integrovaný náhled",
@@ -1021,7 +1233,8 @@
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
"search.valid": "Platné",
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu. Dokud je zapnuto, pohled se překresluje nepřetržitě.",
"settings.frame_rate_details_note": "Přidá graf časů posledních snímků s vykreslenými trojúhelníky, histogram časů snímků a to, co vykreslila GPU. Kliknutím na snímkovou frekvenci v pohledu je zobrazíte nebo skryjete.",
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
"settings.msaa_applies_on_restart": "Vyhlazování se projeví při příštím spuštění SolidCraftu",
@@ -1077,12 +1290,115 @@
"toolbar.customize_note": "Karta {tab}. Zaškrtnutím příkazu přidáte jeho tlačítko na panel nástrojů. Můžete také přetáhnout řádek z rozbalovací nabídky panelu na panel nástrojů, odstranit tlačítko přetažením mimo panel nástrojů nebo použít připínáček na řádku rozbalovací nabídky.",
"toolbar.image": "OBRÁZEK",
"toolbar.render": "RENDER",
"toolbar.render_appearance": "VZHLED",
"toolbar.render_camera": "KAMERA",
"toolbar.render_environment": "PROSTŘEDÍ",
"toolbar.render_lighting": "OSVĚTLENÍ",
"toolbar.render_scene": "SCÉNA",
"trace.points": "Bodů podél cesty: {count}",
"trace.stopped_by_contact": "Končí ve vzorku {sample}: {a} by prošlo skrz {b}",
"trace.stopped_by_loop": "Končí ve vzorku {sample}: smyčku přes {joint} už nelze dále uzavřít",
"ui.1_degree_of_freedom": "1 stupeň volnosti",
"ui.3d_mouse_axes_reassigned": "Osy jsou v Předvolbách přeřazené a to platí i po této kalibraci.",
"ui.3d_mouse_button": "Tlačítko {number}",
"ui.3d_mouse_button_nothing": "Nic",
"ui.3d_mouse_button_settings": "Nastavení 3D myši",
"ui.3d_mouse_buttons": "Tlačítka",
"ui.3d_mouse_buttons_hint": "Stiskněte tlačítko na 3D myši, ať ho tu najdete, a vyberte, co má dělat.",
"ui.3d_mouse_buttons_no_device": "Připojte 3D myš a nastavte, co dělají její tlačítka.",
"ui.3d_mouse_buttons_reset": "Obnovit výchozí tlačítka",
"ui.3d_mouse_calibrate": "Kalibrovat…",
"ui.3d_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
"ui.3d_mouse_calibrate_title": "Kalibrace 3D myši",
"ui.3d_mouse_calibrate_wait": "Jednou pohněte 3D myší, aby ji SolidCraft uviděl.",
"ui.3d_mouse_calibrated": "Zkalibrováno",
"ui.3d_mouse_calibration": "Kalibrace 3D myši",
"ui.3d_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
"ui.3d_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
"ui.3d_mouse_circles_help": "Kruhy ukazují, co zařízení posílá po kalibraci. Zkušební model ukazuje, co s tím udělá pohled po obrácení, rychlosti a přiřazení os.",
"ui.3d_mouse_dead_zone": "Mrtvá zóna",
"ui.3d_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
"ui.3d_mouse_denied": "SolidCraft vidí 3D myš, ale nesmí ji číst. Pravidlo udev najdete v poznámkách k nastavení 3D myši.",
"ui.3d_mouse_denied_short": "Nalezeno, ale nelze číst",
"ui.3d_mouse_driven_by": "Který kalibrovaný pohyb zařízení ovládá tuto osu. Když vyberete pohyb, který už používá jiná osa, obě se prohodí.",
"ui.3d_mouse_driven_by_label": "ovládá",
"ui.3d_mouse_driver": "Ovladač 3D myši",
"ui.3d_mouse_driver_auto": "Automaticky",
"ui.3d_mouse_driver_direct": "Přímo (hidraw)",
"ui.3d_mouse_driver_fallback": "Zatím se čte přes spacenavd: zařízení samotné se nepodařilo otevřít.",
"ui.3d_mouse_driver_only_direct": "Zařízení se čte přímo; spacenavd neběží",
"ui.3d_mouse_driver_only_spacenavd": "Čte se přes spacenavd, jiná možnost není",
"ui.3d_mouse_forget": "Zapomenout kalibraci",
"ui.3d_mouse_ghost_left_viewport": "Vlevo ve výřezu",
"ui.3d_mouse_ghost_left_window": "Vlevo v okně",
"ui.3d_mouse_ghost_position": "Poloha",
"ui.3d_mouse_ghost_right_viewport": "Vpravo ve výřezu",
"ui.3d_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
"ui.3d_mouse_kind_cap": "Návrat do středu",
"ui.3d_mouse_kind_question": "Jaký typ 3D myši to je?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "Hodnota zůstává, dokud ji držíte, jako u zařízení typu „Návrat do středu“.",
"ui.3d_mouse_looks_like_trackball": "Hodnota se vrací zpět, jakmile přestanete hýbat, jako u trackballu.",
"ui.3d_mouse_motion_rotation_x": "Rotace X",
"ui.3d_mouse_motion_rotation_y": "Rotace Y",
"ui.3d_mouse_motion_rotation_z": "Rotace Z",
"ui.3d_mouse_motion_translation_x": "Posun X",
"ui.3d_mouse_motion_translation_y": "Posun Y",
"ui.3d_mouse_motion_translation_z": "Posun Z",
"ui.3d_mouse_navigate": "Navigace pomocí 3D myši",
"ui.3d_mouse_none": "3D myš nenalezena",
"ui.3d_mouse_not_calibrated": "Nezkalibrováno",
"ui.3d_mouse_offer": "Kalibrace 3D myši opraví prohozené nebo obrácené osy a nastaví její rychlost a pocit z ovládání.",
"ui.3d_mouse_offer_never": "Už nezobrazovat",
"ui.3d_mouse_offer_swap": "Přepnout na Hidraw (přímý ovladač)",
"ui.3d_mouse_reading": "Hodnoty",
"ui.3d_mouse_reset_axes": "Obnovit přiřazení os",
"ui.3d_mouse_reset_view": "Obnovit pohled",
"ui.3d_mouse_restore": "Obnovit výchozí nastavení 3D myši",
"ui.3d_mouse_restore_ask": "Vrátit nastavení 3D myši na výchozí hodnoty? Kalibrace a přiřazení tlačítek zůstanou.",
"ui.3d_mouse_section": "3D myš",
"ui.3d_mouse_show_in_view": "Zobrazit 3D myš ve 3D pohledu",
"ui.3d_mouse_show_in_view_tip": "Obrázek vaší 3D myši v levém dolním rohu, který se hýbe s ní.",
"ui.3d_mouse_skip": "Moje zařízení to neumí",
"ui.3d_mouse_skip_tip": "Přeskočí tento pohyb v obou směrech a kalibrace ho vynechá.",
"ui.3d_mouse_speed": "Rychlost 3D myši",
"ui.3d_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
"ui.3d_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
"ui.3d_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
"ui.3d_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
"ui.3d_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
"ui.3d_mouse_step_rest_trackball": "Dejte ruku z kuličky a nechte ji v klidu.",
"ui.3d_mouse_step_ring_clockwise": "Otočte rolovacím kroužkem o pár cvaknutí po směru hodinových ručiček.",
"ui.3d_mouse_step_ring_counterclockwise": "Otočte rolovacím kroužkem o pár cvaknutí proti směru hodinových ručiček.",
"ui.3d_mouse_step_roll_away": "Několikrát kutálejte kuličkou od sebe, pak ji nechte zastavit.",
"ui.3d_mouse_step_roll_left": "Několikrát kutálejte kuličkou doleva, pak ji nechte zastavit.",
"ui.3d_mouse_step_roll_right": "Několikrát kutálejte kuličkou doprava, pak ji nechte zastavit.",
"ui.3d_mouse_step_roll_toward": "Několikrát kutálejte kuličkou k sobě, pak ji nechte zastavit.",
"ui.3d_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
"ui.3d_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
"ui.3d_mouse_step_spin_clockwise": "Několikrát roztočte kuličku po směru hodinových ručiček, pak ji nechte zastavit.",
"ui.3d_mouse_step_spin_counterclockwise": "Několikrát roztočte kuličku proti směru hodinových ručiček, pak ji nechte zastavit.",
"ui.3d_mouse_step_tilt_away": "Nakloňte vršek hlavice od sebe (okraj nejblíž vám jde nahoru), podržte a pusťte.",
"ui.3d_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
"ui.3d_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
"ui.3d_mouse_step_tilt_toward": "Nakloňte vršek hlavice k sobě (okraj nejblíž vám jde dolů), podržte a pusťte.",
"ui.3d_mouse_step_touch": "Položte ruku na hlavici, ale netlačte na ni.",
"ui.3d_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
"ui.3d_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
"ui.3d_mouse_switch_cap": "Kalibrovat jako návrat do středu",
"ui.3d_mouse_switch_trackball": "Kalibrovat jako trackball",
"ui.3d_mouse_view_axis": "Osa pohledu",
"ui.3d_mouse_view_off": "Vypnuto",
"ui.3d_mouse_view_off_tip": "Profil ovladače 3D myši tuto osu vypíná, takže její pohyb s pohledem nehýbe. Když tu vyberete pohyb jiné osy, prohodí se s ní a s pohledem přestane hýbat.",
"ui.3d_mouse_view_orbit_sideways": "Orbit vlevo / vpravo",
"ui.3d_mouse_view_orbit_up_down": "Orbit nahoru / dolů",
"ui.3d_mouse_view_pan_sideways": "Posun vlevo / vpravo",
"ui.3d_mouse_view_pan_up_down": "Posun nahoru / dolů",
"ui.3d_mouse_view_roll": "Natočení",
"ui.3d_mouse_view_unmapped": "Osa {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "Čeká na pohyb",
"ui.3d_mouse_wiggle": "Pokud máte nějakou připojenou, zahýbejte s ní.",
"ui.3d_sketch": "3D náčrt",
"ui.activate": "Aktivovat",
"ui.active_component": "Aktivní komponenta",
@@ -1092,6 +1408,7 @@
"ui.agent_chat_add": "Připojit soubory",
"ui.agent_chat_add_file": "Vybrat soubor…",
"ui.agent_chat_allow": "Povolit",
"ui.agent_chat_allow_once": "Povolit jednou",
"ui.agent_chat_allowed": "Povoleno",
"ui.agent_chat_answered": "Odpověď: {option}",
"ui.agent_chat_approval": "Povolit agentovi tuto akci?",
@@ -1143,12 +1460,16 @@
"ui.agent_chat_files_left_out": "K jedné zprávě lze připojit nejvýše {max} souborů; vynecháno: {files}",
"ui.agent_chat_files_loading": "Čekání na přečtení souborů",
"ui.agent_chat_files_max": "K jedné zprávě nejvýše {max} souborů",
"ui.agent_chat_flatpak_none_found": "Nebyl nalezen žádný kódovací agent. Sandbox Flatpaku ve výchozím stavu nevidí agenty nainstalované ve vaší domovské složce a nemá přístup k síti. Chcete-li povolit Claude Code a jeho adaptér ACP nainstalované přes nvm nebo do ~/.local/bin, spusťte v terminálu tento příkaz a poté SolidCraft restartujte. Udělí přístup k uvedeným programům a přihlašovacím souborům a také přístup k síti:",
"ui.agent_chat_import_failed": "{name}: import selhal: {error}",
"ui.agent_chat_intro": "Chatujte s kódovacím agentem, kterého již používáte, nebo s modelem na zvoleném endpointu. Pracuje na otevřeném návrhu pomocí nástrojů SolidCraftu: jeho změny vidíte průběžně a každou zprávu lze vrátit jedním krokem. Co smí dělat bez ptaní, nastavíte v Předvolby > AI.",
"ui.agent_chat_key_in_env": "klíč nastaven",
"ui.agent_chat_left": "Vlevo",
"ui.agent_chat_limit": "Agent dosáhl limitu {limit} změn na jednu zprávu. Pošlete další zprávu, aby mohl pokračovat.",
"ui.agent_chat_minimize": "Minimalizovat do pruhu",
"ui.agent_chat_mode_all": "Vše",
"ui.agent_chat_mode_ask": "Ptát se",
"ui.agent_chat_mode_edits": "Úpravy",
"ui.agent_chat_model_tip": "Model, který agent používá. Vaše volba se pro tohoto agenta zapamatuje.",
"ui.agent_chat_new": "Nový chat",
"ui.agent_chat_new_confirm": "Začít nový chat? Aktuální bude vymazán.",
@@ -1161,6 +1482,7 @@
"ui.agent_chat_none_found_short": "Agent nenalezen",
"ui.agent_chat_not_installed": "Není nainstalován",
"ui.agent_chat_not_signed_in": "nepřihlášen",
"ui.agent_chat_open_settings": "Otevřít předvolby",
"ui.agent_chat_own_window": "Vlastní okno",
"ui.agent_chat_paste_picture": "Vložit obrázek",
"ui.agent_chat_permission": "Agent žádá o spuštění: {title}",
@@ -1185,6 +1507,8 @@
"ui.agent_chat_send": "Odeslat",
"ui.agent_chat_send_now": "Odeslat hned",
"ui.agent_chat_send_now_tip": "Zastavit aktuální krok agenta a poslat tuto zprávu jako další (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft komunikuje s programovacím agentem nainstalovaným v tomto počítači. Vyberte ho v části Předvolby › AI.",
"ui.agent_chat_setup_title": "Nastavit asistenta",
"ui.agent_chat_side": "Strana chatu s agentem",
"ui.agent_chat_signed_in": "přihlášen",
"ui.agent_chat_state_auth": "Vyžadováno přihlášení",
@@ -1201,6 +1525,7 @@
"ui.agent_chat_undo_label": "Chat s agentem: {message}",
"ui.agent_chat_undone": "(vráceno)",
"ui.agent_chat_view_attached": "Pohled přiložen",
"ui.agent_chat_waiting": "Čeká na vás",
"ui.agent_cursor": "Kurzor agenta",
"ui.agent_cursor_speed": "Rychlost kurzoru agenta",
"ui.ai_add": "Přidat",
@@ -1258,7 +1583,6 @@
"ui.ai_permissions": "Oprávnění",
"ui.ai_preset": "Předvolba",
"ui.ai_remove": "Odebrat",
"ui.ai_request_limit": "zastaveno po 24 požadavcích na jednu zprávu; pro pokračování pošlete další zprávu",
"ui.ai_reset": "Obnovit",
"ui.ai_settings": "Nastavení AI…",
"ui.ai_show_features": "Zobrazit funkce AI",
@@ -1303,6 +1627,7 @@
"ui.blocked": "Blokováno",
"ui.bodies": "Tělesa",
"ui.bodies_all_if_none": "Tělesa (všechna, pokud žádné)",
"ui.bottom": "Zespodu",
"ui.box": "Kvádr",
"ui.break": "Přerušit",
"ui.browser_bodies_one": "{count} těleso",
@@ -1441,6 +1766,7 @@
"ui.flip": "Převrátit",
"ui.format": "Formát",
"ui.frame_rate": "Snímková frekvence",
"ui.frame_rate_details": "Podrobnosti snímkové frekvence",
"ui.front": "Zepředu",
"ui.fully_constrained": "Plně zavazbeno",
"ui.function": "Funkce",
@@ -1455,6 +1781,7 @@
"ui.grid_follows_view_hint": "Boční pohledy zobrazí mřížku na svislé rovině, která je k nim přivrácená, místo na zemi",
"ui.ground_shadow": "Stín na podložce",
"ui.groupSelected": "Seskupit vybrané",
"ui.hatch": "Šrafování",
"ui.heading_add_ins": "DOPLŇKY",
"ui.heading_assemble": "SESTAVA",
"ui.heading_box": "KVÁDR",
@@ -1528,6 +1855,13 @@
"ui.hideOrigin": "Skrýt počátek",
"ui.hidePlanes": "Skrýt roviny",
"ui.hideProfile": "Skrýt profil",
"ui.hide_origin_by_default": "Ve výchozím stavu skrýt počátek",
"ui.hide_origin_note": "Nové návrhy začínají se skrytou složkou Počátek. Návrh si ponechá vlastní nastavení počátku, jakmile ho zobrazíte nebo skryjete.",
"ui.history_back": "O krok zpět",
"ui.history_below_browser": "Pod prohlížečem",
"ui.history_first": "Přejít na začátek",
"ui.history_forward": "O krok vpřed",
"ui.history_last": "Přejít na konec",
"ui.history_title": "Historie",
"ui.hole": "Otvor",
"ui.hole_tap_type": "Typ závitu otvoru",
@@ -1545,6 +1879,8 @@
"ui.joints": "Klouby",
"ui.keyboard_shortcuts": "Klávesové zkratky",
"ui.language": "Jazyk",
"ui.layout_compact": "Kompaktní",
"ui.layout_floating": "Plovoucí",
"ui.length_decimals": "Desetinná místa délky",
"ui.light": "Světlý",
"ui.line": "Úsečka",
@@ -1682,6 +2018,7 @@
"ui.reverse_zoom_direction": "Obrátit směr přiblížení",
"ui.revolve": "Rotace",
"ui.ribbon_exit": "Ukončit",
"ui.ribbon_labels": "Zobrazovat text pod ikonami pásu karet",
"ui.ribbon_pin_footer": "Připnuté příkazy se zobrazují na pásu karet. Najetím na řádek jej připnete nebo odepnete.",
"ui.ribbon_workspace": "Pracovní prostředí",
"ui.right": "Zprava",
@@ -1737,34 +2074,6 @@
"ui.smooth_wheel_zoom": "Plynulé přiblížení kolečkem",
"ui.smooth_wheel_zoom_tip": "Vypnutím se každý pohyb kolečka projeví okamžitě, bez dobíhání",
"ui.snap": "Přichytávání",
"ui.space_mouse_calibrate": "Kalibrovat…",
"ui.space_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
"ui.space_mouse_calibrate_title": "Kalibrace 3D myši",
"ui.space_mouse_calibrated": "Zkalibrováno",
"ui.space_mouse_calibration": "Kalibrace 3D myši",
"ui.space_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
"ui.space_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
"ui.space_mouse_dead_zone": "Mrtvá zóna",
"ui.space_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
"ui.space_mouse_forget": "Zapomenout kalibraci",
"ui.space_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
"ui.space_mouse_navigate": "Navigace pomocí SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "3D myš nenalezena",
"ui.space_mouse_not_calibrated": "Nezkalibrováno",
"ui.space_mouse_reading": "Hodnoty",
"ui.space_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
"ui.space_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
"ui.space_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
"ui.space_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
"ui.space_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
"ui.space_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
"ui.space_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
"ui.space_mouse_step_tilt_back": "Nakloňte hlavici dozadu (přední hrana nahoru), podržte a pusťte.",
"ui.space_mouse_step_tilt_forward": "Nakloňte hlavici dopředu (přední hrana dolů), podržte a pusťte.",
"ui.space_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
"ui.space_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
"ui.space_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
"ui.space_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
"ui.spacing": "Rozteč",
"ui.spacing_2": "Rozteč 2",
"ui.sphere": "Koule",
@@ -1836,6 +2145,7 @@
"ui.workspace_design_tabs": "Model · Plocha · Sestava · Vložit · Kontrola · Správa · Zobrazení",
"ui.workspace_drawing_description": "2D výkresy s pohledy, kótami a popisovými poli",
"ui.workspace_footer": "Všechna pracovní prostředí pracují se stejným návrhem, historií a výběrem; mění se pouze karty pásu karet. Přepínač je zamknutý, dokud se upravuje náčrt nebo prvek.",
"ui.workspace_layout": "Rozložení pracovní plochy",
"ui.workspace_manufacture_description": "Slicing, dráhy nástroje a nastavení",
"ui.workspace_planned": "Plánováno",
"ui.workspace_render_description": "Náhled produktu, nastavení scény a renderované obrázky",
+176 -36
View File
@@ -72,6 +72,7 @@
"Choose…": "Vælg…",
"Circular": "Cirkulær",
"Circumference": "Omkreds",
"Clockwise": "Med uret",
"Close draft": "Luk kladde",
"Collisions": "Kollisioner",
"Colour": "Farve",
@@ -167,6 +168,7 @@
"HISTORY": "HISTORIK",
"Height": "Højde",
"Hidden edges": "Skjulte kanter",
"History layout": "Historiklayout",
"Holes": "Huller",
"Holes (edges on them)": "Huller (kanterne omkring dem)",
"Home view": "Hjemmevisning",
@@ -232,7 +234,7 @@
"No crash reports.": "Ingen nedbrudsrapporter.",
"No matching appearance presets.": "Ingen matchende udseendeforudindstillinger.",
"No matching appearances": "Ingen matchende udseender",
"No visible bodies to export.": "Der er ingen synlige emner at eksportere.",
"No visible bodies or sketches to export.": "Der er ingen synlige emner eller skitser at eksportere.",
"None": "Ingen",
"Not available yet": "Endnu ikke tilgængelig",
"Notch": "Udskæring",
@@ -321,6 +323,7 @@
"Search commands": "Søg efter kommandoer",
"Search this library": "Søg i dette bibliotek",
"Section": "Snit",
"Section position": "Profilets placering",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vælg andet: Hold venstre museknap nede uden at flytte markøren på modellen (eller højreklik › Vælg andet…) for at vise alt under markøren. Klik igen samme sted for at vælge næste objekt. Dobbeltklik på den midterste museknap for at gå til Hjem (Indstillinger › Navigation).",
"Select bodies (a face picks its body)": "Vælg emner (en flade vælger sit emne)",
"Select bodies or faces instead": "Vælg emner eller flader i stedet",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Vis agentmarkør",
"Show in folder": "Vis i mappe",
"Show the pointer an AI agent moves (#35)": "Vis markøren, som en AI-agent bevæger (#35)",
"Show tool at cursor": "Vis værktøj ved markøren",
"Signed axes": "Akser med fortegn",
"Size": "Størrelse",
"Sketch constraints": "Skitsebindinger",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft lukkede, før disse designs blev gemt. Den seneste automatiske lagring kan åbnes.",
"Specialise": "Specialisér",
"Speed": "Hastighed",
"Square": "Kvadrat",
"Stand it up": "Stil op",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stil op: En kvart omdrejning om X som én flyttefeature, der kan fortrydes.",
"Stand this part up?": "Stil dette emne op?",
"Start angle": "Startvinkel",
"Start bulge": "Udbuling ved starten",
"Start continuity": "Overgang ved starten",
"Step": "Trin",
@@ -411,6 +417,7 @@
"angle, height": "vinkel, højde",
"appearance.dielectric_ior": "Dielektrisk brydningsindeks",
"appearance.metallic": "Metallisk",
"appearance.only_available": "Kun tilgængelige udseender",
"appearance.pbr_help": "Se denne overflade i Rendering eller i en billedfil. Lyset brydes ikke og passerer ikke gennem overfladen.",
"appearance.pbr_opaque": "Uigennemsigtig PBR",
"appearance.roughness": "Ruhed",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Indsæt STEP",
"cmd.file_save_mesh": "Gem som mesh",
"cmd.file_slicers": "Installerede slicere",
"cmd.frame_member": "Rammeelement",
"cmd.frame_profile_insert": "Indsæt profil",
"cmd.frame_profile_list": "Profilbibliotek",
"cmd.frame_profile_properties": "Profilegenskaber",
"cmd.inspect_accessibility": "Tilgængelighedsanalyse",
"cmd.inspect_center_of_mass": "Massecentrum",
"cmd.inspect_clearance": "Frigangskontrol",
@@ -568,6 +579,7 @@
"cmd.material_create": "Nyt fysisk materiale",
"cmd.material_delete": "Slet fysisk materiale",
"cmd.material_edit": "Redigér fysisk materiale",
"cmd.material_inspect": "Inspicér fysisk materiale",
"cmd.material_list": "Vis materialer",
"cmd.material_properties_directional": "Retningsafhængigt elasticitetsmodul",
"cmd.material_properties_validate": "Validér fysiske materialedata",
@@ -831,7 +843,7 @@
"dxf.progress.projection": "Projicerer geometri (3/5)",
"dxf.progress.write": "Skriver DXF (5/5)",
"dxf.tolerance": "Kurvetolerance",
"dxf.tolerance_help": "Kun synlige emner. Kurver består af rette linjestykker. En mindre tolerance giver finere kurver og kræver mere tid og hukommelse.",
"dxf.tolerance_help": "Kun synlige emner og viste skitser. Kurver består af rette linjestykker. En mindre tolerance giver finere kurver og kræver mere tid og hukommelse.",
"file_load.built": "Bygget indtil videre: {built} af {total} features.",
"file_load.converting": "Konverterer solid {done} af {total}",
"file_load.inserting": "Indsætter geometri",
@@ -856,19 +868,57 @@
"help.mit": "MIT-licens",
"help.notice": "NOTICE",
"help.third_party": "Tredjepartslicenser",
"home.back": "Tilbage til {name}",
"home.crashes": "Nedbrudsrapporter",
"home.design_workspace": "Design",
"home.documentation": "Dokumentation",
"home.export": "Eksportér",
"home.export_hint": "Vælg et format til det aktuelle design.",
"home.export_json_hint": "Redigerbart design og kommandohistorik",
"home.export_mesh_hint": "Mesh til udskrivning eller deling",
"home.export_step_hint": "Præcis geometri for massive legemer",
"home.import": "Importér…",
"home.import_hint": "STEP, IGES eller mesh til et nyt design",
"home.in_this_browser": "i denne webbrowser",
"home.missing": "mangler",
"home.month_01": "jan.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "apr.",
"home.month_05": "maj",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "aug.",
"home.month_09": "sep.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Nyt design i tommer",
"home.new_inches_hint": "Tommer · åbner i Design",
"home.new_mm": "Nyt design",
"home.new_mm_hint": "Millimeter · åbner i Design",
"home.open": "Åbn…",
"home.opened_days_ago": "Åbnet for {count} dage siden",
"home.opened_hours_ago": "Åbnet for {count} t siden",
"home.opened_just_now": "Netop åbnet",
"home.opened_minutes_ago": "Åbnet for {count} min siden",
"home.open_hint": "Et .solidcraft-design ({shortcut})",
"home.open_now": "Åbn nu",
"home.opened_days_ago": "For {count} dage siden",
"home.opened_hours_ago": "For {count} t siden",
"home.opened_just_now": "Lige nu",
"home.opened_minutes_ago": "For {count} min siden",
"home.opening": "åbner…",
"home.properties": "Designegenskaber",
"home.recent": "Seneste",
"home.recent_empty": "Design, du åbner eller gemmer, vises her.",
"home.recovery_count": "Design til gendannelse: {count}",
"home.recovery_empty": "Ingen design kan gendannes.",
"home.recovery_hint": "SolidCraft lukkede, før disse design blev gemt.",
"home.report_issue": "Rapportér et problem",
"home.samples": "Eksempler",
"home.settings": "Indstillinger",
"home.short_date": "{day}. {month}",
"home.shortcuts": "Tastaturgenveje",
"home.start_hint": "Opret et design, åbn en nylig fil, eller udforsk et eksempel.",
"home.whats_new": "Nyheder",
"home.yesterday": "I går",
"install.button_tip": "Installér SolidCraft som en app: Den åbner i sit eget vindue og virker offline",
"install.dock_1": "Vælg Arkiv › Føj til Dock.",
"install.dock_2": "Klik på Tilføj.",
@@ -1021,7 +1071,7 @@
"search.try": "Skriv et kommandonavn, en feature i dette design eller en parameter. Du kører en kommando med værdier ved at skrive dens id og et JSON-objekt: {example}",
"search.valid": "Gyldig",
"settings.display_quality_note": "Finere niveauer viser krumme flader og kanter mere jævnt og tager længere tid at opbygge mesh til. Kun modelvisningen ændres, aldrig designet eller eksporten.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen. Når dette er slået til, gentegnes visningen løbende.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen.",
"settings.measure_axis_distances": "Vis X/Y/Z-afstande",
"settings.measure_axis_distances_help": "Vis afstanden langs X-, Y- og Z-aksen sammen med den samlede afstand i Mål.",
"settings.msaa_applies_on_restart": "Kantudjævning træder i kraft, næste gang SolidCraft starter",
@@ -1082,7 +1132,106 @@
"trace.stopped_by_contact": "Slutter ved prøve {sample}: {a} ville gå gennem {b}",
"trace.stopped_by_loop": "Slutter ved prøve {sample}: Løkken gennem {joint} kan ikke lukkes yderligere",
"ui.1_degree_of_freedom": "1 frihedsgrad",
"ui.3d_mouse_axes_reassigned": "Akserne er omtildelt i Indstillinger, og det gælder stadig oven på denne kalibrering.",
"ui.3d_mouse_button": "Knap {number}",
"ui.3d_mouse_button_nothing": "Ingenting",
"ui.3d_mouse_button_settings": "Indstillinger for 3D-mus",
"ui.3d_mouse_buttons": "Knapper",
"ui.3d_mouse_buttons_hint": "Tryk på en knap på din 3D-mus for at finde den her, og vælg så, hvad den gør.",
"ui.3d_mouse_buttons_no_device": "Tilslut en 3D-mus for at vælge, hvad dens knapper gør.",
"ui.3d_mouse_buttons_reset": "Gendan standardknapper",
"ui.3d_mouse_calibrate": "Kalibrér…",
"ui.3d_mouse_calibrate_tip": "Bevæg knoppen som vist i vinduet, så SolidCraft kan registrere retningen og bevægelsesområdet for hver akse. Indstillingerne gemmes til denne 3D-mus.",
"ui.3d_mouse_calibrate_title": "Kalibrér 3D-mus",
"ui.3d_mouse_calibrate_wait": "Bevæg 3D-musen én gang, så SolidCraft kan se den.",
"ui.3d_mouse_calibrated": "Kalibreret",
"ui.3d_mouse_calibration": "Kalibrering af 3D-mus",
"ui.3d_mouse_calibration_done": "Færdig. Gem kalibreringen, så den bruges, hver gang denne 3D-mus tilsluttes.",
"ui.3d_mouse_calibration_no_response": "En af akserne bevægede sig for lidt til at kunne kalibreres. Start forfra, og bevæg knoppen længere hver gang.",
"ui.3d_mouse_circles_help": "Cirklerne viser, hvad enheden sender efter kalibrering. Testmodellen viser, hvad visningen gør med det efter omvending, hastighed og akseplacering.",
"ui.3d_mouse_dead_zone": "Dødzone",
"ui.3d_mouse_dead_zone_tip": "Hvor langt knoppen skal bevæges, før visningen reagerer, angivet som en andel af dens fulde bevægelse. Øg værdien, hvis visningen bevæger sig, når du ikke rører knoppen.",
"ui.3d_mouse_denied": "SolidCraft kan se en 3D-mus, men må ikke læse den. Se opsætningsnoterne til 3D-mus for udev-reglen.",
"ui.3d_mouse_denied_short": "Fundet, men kan ikke læses",
"ui.3d_mouse_driven_by": "Hvilken kalibreret bevægelse af enheden der styrer denne akse. Vælger du en bevægelse, som en anden akse allerede bruger, bytter de to plads.",
"ui.3d_mouse_driven_by_label": "styres af",
"ui.3d_mouse_driver": "3D-musedriver",
"ui.3d_mouse_driver_auto": "Automatisk",
"ui.3d_mouse_driver_direct": "Direkte (hidraw)",
"ui.3d_mouse_driver_fallback": "Læser indtil videre gennem spacenavd: selve enheden kunne ikke åbnes.",
"ui.3d_mouse_driver_only_direct": "Læser enheden direkte; spacenavd kører ikke",
"ui.3d_mouse_driver_only_spacenavd": "Læser gennem spacenavd, den eneste mulighed",
"ui.3d_mouse_forget": "Slet kalibrering",
"ui.3d_mouse_ghost_left_viewport": "Venstre visningsområde",
"ui.3d_mouse_ghost_left_window": "Venstre vindue",
"ui.3d_mouse_ghost_position": "Placering",
"ui.3d_mouse_ghost_right_viewport": "Højre visningsområde",
"ui.3d_mouse_hint": "Flyt og rotér visningen samtidigt; skitser bruger kun panorering og zoom.",
"ui.3d_mouse_kind_cap": "Vend tilbage til midten",
"ui.3d_mouse_kind_question": "Hvilken slags 3D-mus er det?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "Værdien bliver, mens du holder, som på en enhed af typen „Vend tilbage til midten“.",
"ui.3d_mouse_looks_like_trackball": "Værdien falder tilbage, så snart du stopper, som på en trackball.",
"ui.3d_mouse_motion_rotation_x": "Rotation X",
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
"ui.3d_mouse_motion_translation_x": "Forskydning X",
"ui.3d_mouse_motion_translation_y": "Forskydning Y",
"ui.3d_mouse_motion_translation_z": "Forskydning Z",
"ui.3d_mouse_navigate": "Navigér med 3D-musen",
"ui.3d_mouse_none": "Ingen 3D-mus fundet",
"ui.3d_mouse_not_calibrated": "Ikke kalibreret",
"ui.3d_mouse_offer": "Kalibrering af din 3D-mus retter ombyttede eller vendte akser og indstiller dens hastighed og fornemmelse.",
"ui.3d_mouse_offer_never": "Vis ikke igen",
"ui.3d_mouse_offer_swap": "Skift til Hidraw (direkte driver)",
"ui.3d_mouse_reading": "Aflæser",
"ui.3d_mouse_reset_axes": "Nulstil aksetildeling",
"ui.3d_mouse_reset_view": "Nulstil visning",
"ui.3d_mouse_restore": "Gendan 3D-musens standardværdier",
"ui.3d_mouse_restore_ask": "Sæt 3D-musens indstillinger tilbage til standard? Din kalibrering og knaptildeling bliver.",
"ui.3d_mouse_section": "3D-mus",
"ui.3d_mouse_show_in_view": "Vis 3D-musen i 3D-visningen",
"ui.3d_mouse_show_in_view_tip": "Et billede af din 3D-mus i nederste venstre hjørne, der bevæger sig, når du bevæger den.",
"ui.3d_mouse_skip": "Min enhed kan ikke det",
"ui.3d_mouse_skip_tip": "Springer bevægelsen over begge veje, og kalibreringen udelader den.",
"ui.3d_mouse_speed": "3D-musens hastighed",
"ui.3d_mouse_step_lift": "Løft knoppen op, hold den, og slip.",
"ui.3d_mouse_step_press": "Tryk knoppen ned, hold den, og slip.",
"ui.3d_mouse_step_pull": "Træk knoppen mod dig, hold den, og slip.",
"ui.3d_mouse_step_push": "Skub knoppen væk fra dig, hold den, og slip.",
"ui.3d_mouse_step_rest": "Fjern hånden fra knoppen, og lad den stå stille.",
"ui.3d_mouse_step_rest_trackball": "Fjern hånden fra kuglen, og lad den ligge stille.",
"ui.3d_mouse_step_ring_clockwise": "Drej rullehjulet et par klik med uret.",
"ui.3d_mouse_step_ring_counterclockwise": "Drej rullehjulet et par klik mod uret.",
"ui.3d_mouse_step_roll_away": "Rul kuglen væk fra dig et par gange, og lad den stoppe.",
"ui.3d_mouse_step_roll_left": "Rul kuglen mod venstre et par gange, og lad den stoppe.",
"ui.3d_mouse_step_roll_right": "Rul kuglen mod højre et par gange, og lad den stoppe.",
"ui.3d_mouse_step_roll_toward": "Rul kuglen ind mod dig et par gange, og lad den stoppe.",
"ui.3d_mouse_step_slide_left": "Skub knoppen mod venstre, hold den, og slip.",
"ui.3d_mouse_step_slide_right": "Skub knoppen mod højre, hold den, og slip.",
"ui.3d_mouse_step_spin_clockwise": "Drej kuglen rundt med uret et par gange, og lad den stoppe.",
"ui.3d_mouse_step_spin_counterclockwise": "Drej kuglen rundt mod uret et par gange, og lad den stoppe.",
"ui.3d_mouse_step_tilt_away": "Vip toppen af knoppen væk fra dig (kanten nærmest dig går op), hold den, og slip.",
"ui.3d_mouse_step_tilt_left": "Vip knoppen mod venstre (venstre kant ned), hold den, og slip.",
"ui.3d_mouse_step_tilt_right": "Vip knoppen mod højre (højre kant ned), hold den, og slip.",
"ui.3d_mouse_step_tilt_toward": "Vip toppen af knoppen ind mod dig (kanten nærmest dig går ned), hold den, og slip.",
"ui.3d_mouse_step_touch": "Hvil hånden på knoppen uden at skubbe til den.",
"ui.3d_mouse_step_twist_clockwise": "Drej knoppen med uret, hold den, og slip.",
"ui.3d_mouse_step_twist_counterclockwise": "Drej knoppen mod uret, hold den, og slip.",
"ui.3d_mouse_switch_cap": "Kalibrer som vend tilbage til midten",
"ui.3d_mouse_switch_trackball": "Kalibrér som trackball",
"ui.3d_mouse_view_axis": "Visningsakse",
"ui.3d_mouse_view_off": "Fra",
"ui.3d_mouse_view_off_tip": "3D-musedriverens profil slår denne akse fra, så dens bevægelse flytter ikke visningen. Vælger du en anden akses bevægelse her, bytter de to plads, og den bevægelse flytter ikke længere visningen.",
"ui.3d_mouse_view_orbit_sideways": "Orbit venstre / højre",
"ui.3d_mouse_view_orbit_up_down": "Orbit op / ned",
"ui.3d_mouse_view_pan_sideways": "Panorér venstre / højre",
"ui.3d_mouse_view_pan_up_down": "Panorér op / ned",
"ui.3d_mouse_view_roll": "Rul",
"ui.3d_mouse_view_unmapped": "Akse {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "Venter på bevægelse",
"ui.3d_mouse_wiggle": "Hvis du har en tilsluttet, så bevæg den lidt.",
"ui.3d_sketch": "3D-skitse",
"ui.activate": "Aktivér",
"ui.active_component": "Aktiv komponent",
@@ -1092,6 +1241,7 @@
"ui.agent_chat_add": "Vedhæft filer",
"ui.agent_chat_add_file": "Vælg en fil…",
"ui.agent_chat_allow": "Tillad",
"ui.agent_chat_allow_once": "Tillad én gang",
"ui.agent_chat_allowed": "Tilladt",
"ui.agent_chat_answered": "Svar: {option}",
"ui.agent_chat_approval": "Tillad agenten at gøre dette?",
@@ -1143,12 +1293,16 @@
"ui.agent_chat_files_left_out": "Højst {max} filer pr. besked; udeladt: {files}",
"ui.agent_chat_files_loading": "Venter på indlæsning af filerne",
"ui.agent_chat_files_max": "Højst {max} filer i én besked",
"ui.agent_chat_flatpak_none_found": "Ingen kodeagent fundet. Flatpak-sandkassen kan som standard ikke se agenter, der er installeret i din hjemmemappe, og har ingen netværksadgang. For at tillade Claude Code og dets ACP-adapter installeret via nvm eller i ~/.local/bin skal du køre denne kommando i en terminal og derefter genstarte SolidCraft. Den giver adgang til de nævnte programmer og loginfiler samt netværksadgang:",
"ui.agent_chat_import_failed": "{name}: Importen mislykkedes: {error}",
"ui.agent_chat_intro": "Chat med en kodeagent, du allerede bruger, eller en modeltjeneste, du vælger. Den arbejder på det åbne design gennem SolidCrafts værktøjer: Ændringer vises undervejs og fortrydes som ét trin pr. besked. Hvad den må gøre uden at spørge, angives i Indstillinger > AI.",
"ui.agent_chat_key_in_env": "nøgle angivet",
"ui.agent_chat_left": "Venstre",
"ui.agent_chat_limit": "Agenten nåede grænsen på {limit} ændringer for én besked. Send en ny besked for at lade den fortsætte.",
"ui.agent_chat_minimize": "Minimér til en stribe",
"ui.agent_chat_mode_all": "Alt",
"ui.agent_chat_mode_ask": "Spørg",
"ui.agent_chat_mode_edits": "Ændringer",
"ui.agent_chat_model_tip": "Modellen, som agenten bruger. Dit valg huskes for denne agent.",
"ui.agent_chat_new": "Ny chat",
"ui.agent_chat_new_confirm": "Start en ny chat? Den aktuelle chat ryddes.",
@@ -1161,6 +1315,7 @@
"ui.agent_chat_none_found_short": "Ingen agent fundet",
"ui.agent_chat_not_installed": "Ikke installeret",
"ui.agent_chat_not_signed_in": "ikke logget ind",
"ui.agent_chat_open_settings": "Åbn indstillinger",
"ui.agent_chat_own_window": "Eget vindue",
"ui.agent_chat_paste_picture": "Indsæt et billede",
"ui.agent_chat_permission": "Agenten beder om at køre: {title}",
@@ -1185,6 +1340,8 @@
"ui.agent_chat_send": "Send",
"ui.agent_chat_send_now": "Send nu",
"ui.agent_chat_send_now_tip": "Stop agentens aktuelle trin, og send denne som den næste (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft kommunikerer med en kodeagent, der er installeret på denne computer. Vælg en under Indstillinger › AI.",
"ui.agent_chat_setup_title": "Opsæt en assistent",
"ui.agent_chat_side": "Agentchattens side",
"ui.agent_chat_signed_in": "logget ind",
"ui.agent_chat_state_auth": "Login kræves",
@@ -1201,6 +1358,7 @@
"ui.agent_chat_undo_label": "Agentchat: {message}",
"ui.agent_chat_undone": "(fortrudt)",
"ui.agent_chat_view_attached": "Visning vedhæftet",
"ui.agent_chat_waiting": "Venter på dig",
"ui.agent_cursor": "Agentmarkør",
"ui.agent_cursor_speed": "Agentmarkørens hastighed",
"ui.ai_add": "Tilføj",
@@ -1258,7 +1416,6 @@
"ui.ai_permissions": "Tilladelser",
"ui.ai_preset": "Forudindstilling",
"ui.ai_remove": "Fjern",
"ui.ai_request_limit": "stoppet efter 24 anmodninger for én besked; send en ny besked for at fortsætte",
"ui.ai_reset": "Nulstil",
"ui.ai_settings": "AI-indstillinger…",
"ui.ai_show_features": "Vis AI-funktioner",
@@ -1303,6 +1460,7 @@
"ui.blocked": "Blokeret",
"ui.bodies": "Emner",
"ui.bodies_all_if_none": "Emner (alle, hvis ingen er valgt)",
"ui.bottom": "Nedenfra",
"ui.box": "Boks",
"ui.break": "Bryd",
"ui.browser_bodies_one": "{count} emne",
@@ -1455,6 +1613,7 @@
"ui.grid_follows_view_hint": "Sidevisninger viser gitteret på det lodrette plan, der vender mod dem, i stedet for grundplanet",
"ui.ground_shadow": "Skygge på grundplan",
"ui.groupSelected": "Gruppér valgte",
"ui.hatch": "Skravering",
"ui.heading_add_ins": "ADD-INS",
"ui.heading_assemble": "SAMLING",
"ui.heading_box": "BOKS",
@@ -1528,6 +1687,11 @@
"ui.hideOrigin": "Skjul nulpunkt",
"ui.hidePlanes": "Skjul planer",
"ui.hideProfile": "Skjul profil",
"ui.history_back": "Gå ét trin tilbage",
"ui.history_below_browser": "Under browseren",
"ui.history_first": "Gå til begyndelsen",
"ui.history_forward": "Gå ét trin frem",
"ui.history_last": "Gå til slutningen",
"ui.history_title": "Historik",
"ui.hole": "Hul",
"ui.hole_tap_type": "Hullets gevindtype",
@@ -1545,6 +1709,8 @@
"ui.joints": "Led",
"ui.keyboard_shortcuts": "Tastaturgenveje",
"ui.language": "Sprog",
"ui.layout_compact": "Kompakt",
"ui.layout_floating": "Flydende",
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
@@ -1682,6 +1848,7 @@
"ui.reverse_zoom_direction": "Vend zoomretning",
"ui.revolve": "Omdrejning",
"ui.ribbon_exit": "Afslut",
"ui.ribbon_labels": "Vis tekst under ikonerne på båndet",
"ui.ribbon_pin_footer": "Fastgjorte kommandoer vises på båndet. Hold musen over en række for at fastgøre eller frigøre den.",
"ui.ribbon_workspace": "Arbejdsområde",
"ui.right": "Fra højre",
@@ -1737,34 +1904,6 @@
"ui.smooth_wheel_zoom": "Jævn zoom med musehjul",
"ui.smooth_wheel_zoom_tip": "Slå fra for at anvende hver hjulbevægelse straks uden efterfølgende udjævning",
"ui.snap": "Fastgør til geometri",
"ui.space_mouse_calibrate": "Kalibrér…",
"ui.space_mouse_calibrate_tip": "Bevæg knoppen som vist i vinduet, så SolidCraft kan registrere retningen og bevægelsesområdet for hver akse. Indstillingerne gemmes til denne 3D-mus.",
"ui.space_mouse_calibrate_title": "Kalibrér 3D-mus",
"ui.space_mouse_calibrated": "Kalibreret",
"ui.space_mouse_calibration": "Kalibrering af 3D-mus",
"ui.space_mouse_calibration_done": "Færdig. Gem indstillingerne, så de bruges, hver gang denne 3D-mus tilsluttes.",
"ui.space_mouse_calibration_no_response": "En af akserne bevægede sig for lidt til at kunne kalibreres. Start forfra, og bevæg knoppen længere hver gang.",
"ui.space_mouse_dead_zone": "Dødzone",
"ui.space_mouse_dead_zone_tip": "Hvor langt knoppen skal bevæges, før visningen reagerer, angivet som en andel af dens fulde bevægelse. Øg værdien, hvis visningen bevæger sig, når du ikke rører knoppen.",
"ui.space_mouse_forget": "Nulstil kalibrering",
"ui.space_mouse_hint": "Flyt og rotér visningen samtidigt; skitser bruger kun panorering og zoom.",
"ui.space_mouse_navigate": "Navigér med SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Ingen 3D-mus fundet",
"ui.space_mouse_not_calibrated": "Ikke kalibreret",
"ui.space_mouse_reading": "Aflæser",
"ui.space_mouse_step_lift": "Løft knoppen op, hold den dér, og slip.",
"ui.space_mouse_step_press": "Tryk knoppen ned, hold den dér, og slip.",
"ui.space_mouse_step_pull": "Træk knoppen mod dig, hold den dér, og slip.",
"ui.space_mouse_step_push": "Skub knoppen væk fra dig, hold den dér, og slip.",
"ui.space_mouse_step_rest": "Fjern hånden fra knoppen, og lad den stå stille.",
"ui.space_mouse_step_slide_left": "Skub knoppen mod venstre, hold den dér, og slip.",
"ui.space_mouse_step_slide_right": "Skub knoppen mod højre, hold den dér, og slip.",
"ui.space_mouse_step_tilt_back": "Vip knoppen bagud (forkanten op), hold den dér, og slip.",
"ui.space_mouse_step_tilt_forward": "Vip knoppen fremad (forkanten ned), hold den dér, og slip.",
"ui.space_mouse_step_tilt_left": "Vip knoppen mod venstre (venstre kant ned), hold den dér, og slip.",
"ui.space_mouse_step_tilt_right": "Vip knoppen mod højre (højre kant ned), hold den dér, og slip.",
"ui.space_mouse_step_twist_clockwise": "Drej knoppen med uret, hold den dér, og slip.",
"ui.space_mouse_step_twist_counterclockwise": "Drej knoppen mod uret, hold den dér, og slip.",
"ui.spacing": "Afstand",
"ui.spacing_2": "Afstand 2",
"ui.sphere": "Kugle",
@@ -1836,6 +1975,7 @@
"ui.workspace_design_tabs": "Model · Overflade · Saml · Indsæt · Undersøg · Administrér · Visning",
"ui.workspace_drawing_description": "2D-tegninger med visninger, mål og tegningshoveder",
"ui.workspace_footer": "Alle arbejdsområder arbejder på det samme design, den samme historik og den samme markering; kun båndets faner ændres. Skifteren er låst, mens en skitse eller feature redigeres.",
"ui.workspace_layout": "Arbejdsområdets layout",
"ui.workspace_manufacture_description": "Slicing, værktøjsbaner og opsætning",
"ui.workspace_planned": "Planlagt",
"ui.workspace_render_description": "Produktforhåndsvisning, scene-indstillinger og renderede billeder",
+444 -38
View File
File diff suppressed because it is too large Load Diff
+81 -1
View File
@@ -7,6 +7,8 @@
"Modeled": "Modelled",
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
"cmd.inspect_center_of_mass": "Centre of Mass",
"cmd.render_aging_describe": "Layered Surface Aging",
"cmd.render_effects_describe": "Lens and Creative Effects",
"cmd.sketch_arc_center_point": "Centre Point Arc",
"cmd.sketch_circle_center": "Centre Diameter Circle",
"cmd.sketch_rectangle_center": "Centre Rectangle",
@@ -14,14 +16,92 @@
"cmd.sketch_slot_center_point": "Centre Point Slot",
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"render.aging_add": "Add aging layer",
"render.aging_airborne": "Airborne dust / participating medium",
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
"render.aging_all_bodies": "All eligible opaque bodies",
"render.aging_chips": "Chipped paint",
"render.aging_class_any": "All opaque material classes",
"render.aging_class_dielectric": "Nonmetallic surfaces only",
"render.aging_class_metal": "Metallic surfaces only",
"render.aging_color": "Layer colour · sRGB",
"render.aging_coverage": "Coverage",
"render.aging_dust": "Deposited dust",
"render.aging_empty": "No aging layers: the original surfaces remain unchanged.",
"render.aging_fading": "Faded coating",
"render.aging_grime": "Grime & staining",
"render.aging_help": "Combine up to eight optical layers, in order. Rust and patina are authored appearances, not age/corrosion simulations. Scale is in world millimetres. No geometry, mass or engineering property is changed; glass and passive stages are excluded.",
"render.aging_metallic": "Layer metallic fraction",
"render.aging_origin": "Field origin · world mm",
"render.aging_patina": "Copper-green patina",
"render.aging_refresh_targets": "Inspect current bodies",
"render.aging_roughness": "Layer roughness",
"render.aging_rust": "Oxidized iron / rust",
"render.aging_scale": "Pattern scale",
"render.aging_scratches": "Fine scratches",
"render.aging_target": "Target & coordinates",
"render.aging_target_help": "A named target must still match its captured index and name. Global masks are world-anchored; they do not follow moving parts. This is neither cavity detection nor true edge wear. Filter the list to reach additional bodies.",
"render.aging_target_search": "Filter body names",
"render.aging_upward": "Upward-facing deposition",
"render.backdrop_x": "Centre X",
"render.backdrop_y": "Centre Y",
"render.background_camera_section": "Camera background · PNG presentation",
"render.background_completed_frozen": "The displayed result is a frozen completed image. Editing a background does not repaint that result automatically. Use Regrade for camera-only changes, or Render for a new physical stage.",
"render.background_hdr_separate": "The current HDR resources illuminate and reflect on surfaces; they are not automatically displayed as a panorama behind the subject. Changing a gradient does not rotate or recolour the lighting.",
"render.background_lighting_section": "HDR illumination · separate from camera background",
"render.background_no_alpha_override": "A camera background is not added to transparent output silently. Raw-master regrading uses captured coverage and does not reapply denoising; the source EXR remains unchanged.",
"render.background_regrade": "Apply to retained raw master",
"render.background_stage_occlusion": "This shot includes physical stage geometry. A large floor or wall can fill the frame and hide a camera-only background. Clear or resize the stage deliberately, or change its physical reflectance. Regrading cannot remove captured geometry.",
"render.background_workflow": "Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
"render.chromatic_offset": "Chromatic edge offset",
"render.effects_gpu_only": "These effects require GPU path tracing. Changing the page never switches the renderer automatically.",
"render.effects_help": "Combine ordered colour looks and image-space lens effects. Rendering and raw-master regrading use the same saved settings. Raw EXR stays unchanged. These are original artistic controls, not calibrated lens or branded-film emulations.",
"render.effects_reset_lens": "Reset lens effects",
"render.effects_seed": "Pattern seed",
"render.film_grain": "Photographic grain",
"render.flare_ghosts": "Ghost rings per source",
"render.flare_halo": "Flare halo",
"render.flare_streak": "Anamorphic-style streak",
"render.flare_strength": "Highlight flare strength",
"render.flare_threshold": "Scene-linear flare threshold",
"render.layer_down": "Move down",
"render.layer_enabled": "Layer enabled",
"render.layer_remove": "Remove layer",
"render.layer_strength": "Layer strength",
"render.layer_up": "Move up",
"render.lens_effects_help": "Flare uses up to four separated bright sources in the actual linear image. Ghosts, halo, streaks, chromatic offsets and radial distortion are artistic image-space approximations. Spatial effects need opaque output; outside samples extend the image edge. Grain is deterministic. No physics, EXR or geometry is altered.",
"render.lens_effects_title": "Lens-inspired & photographic",
"render.look_add": "Add colour look",
"render.look_bleach": "Bleach-bypass-inspired",
"render.look_cool": "Cool",
"render.look_cyanotype": "Cyanotype-inspired",
"render.look_faded": "Faded print",
"render.look_mono": "Monochrome",
"render.look_sepia": "Sepia",
"render.look_strength": "Look strength",
"render.look_teal_orange": "Teal / orange",
"render.look_title": "Creative look stack",
"render.look_warm": "Warm",
"render.opaque_preview_help": "Opaque PBR and simple colours; blended transparency. Refraction, HDRI and final rendering are not available yet.",
"render.page_aging": "Aging & contamination",
"render.page_background": "Backgrounds",
"render.page_effects": "Lens & finishing effects",
"render.radial_distortion": "Radial distortion",
"render.ribbon_aging": "Aging",
"render.ribbon_effects": "Effects",
"render.sharpening": "Detail sharpening",
"render.soft_focus": "Soft focus",
"sketch_text.align_center": "Centre",
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
"ui.center": "Centre",
"ui.centerline": "Centreline",
"ui.customizeToolbar": "Customise Toolbar…",
"ui.design_properties.license": "Licence",
"ui.design_properties.organization": "Organisation",
"ui.space_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
"ui.layout_compact": "Compact",
"ui.layout_floating": "Floating",
"ui.ribbon_labels": "Show text under ribbon icons",
"ui.workspace_layout": "Workspace layout",
"ui.zoom_to_cursor_tip": "The wheel zooms towards the point under the cursor (off: towards the view's centre)",
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software centre. SolidCraft doesn't check."
}
+1288 -41
View File
File diff suppressed because it is too large Load Diff
+833 -45
View File
File diff suppressed because it is too large Load Diff
+193 -45
View File
@@ -72,6 +72,7 @@
"Choose…": "Choisir…",
"Circular": "Circulaire",
"Circumference": "Circonférence",
"Clockwise": "Horaire",
"Close draft": "Fermer le brouillon",
"Collisions": "Collisions",
"Colour": "Couleur",
@@ -167,6 +168,7 @@
"HISTORY": "HISTORIQUE",
"Height": "Hauteur",
"Hidden edges": "Arêtes cachées",
"History layout": "Disposition de l’historique",
"Holes": "Trous",
"Holes (edges on them)": "Trous (par leurs arêtes)",
"Home view": "Vue d'accueil",
@@ -232,7 +234,7 @@
"No crash reports.": "Aucun rapport de plantage.",
"No matching appearance presets.": "Aucun préréglage d’apparence correspondant.",
"No matching appearances": "Aucune apparence correspondante",
"No visible bodies to export.": "Aucun corps visible à exporter.",
"No visible bodies or sketches to export.": "Aucun corps ni esquisse visible à exporter.",
"None": "Aucun",
"Not available yet": "Pas encore disponible",
"Notch": "Encoche",
@@ -321,6 +323,7 @@
"Search commands": "Rechercher des commandes",
"Search this library": "Rechercher dans cette bibliothèque",
"Section": "Coupe",
"Section position": "Position de la section",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Sélectionner un autre élément : maintenir le bouton gauche immobile sur le modèle (ou clic droit › Sélectionner un autre…) pour lister tout ce qui se trouve sous le curseur ; cliquer de nouveau au même endroit pour sélectionner l'élément suivant. Double-cliquer avec le bouton du milieu pour revenir à l'accueil (Préférences › Navigation).",
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Afficher le curseur de l'agent",
"Show in folder": "Afficher dans le dossier",
"Show the pointer an AI agent moves (#35)": "Afficher le pointeur déplacé par un agent IA (#35)",
"Show tool at cursor": "Afficher l'outil près du curseur",
"Signed axes": "Axes signés",
"Size": "Taille",
"Sketch constraints": "Contraintes d'esquisse",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
"Specialise": "Spécialiser",
"Speed": "Vitesse",
"Square": "Carré",
"Stand it up": "Redresser",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Redresser : un quart de tour autour de X, sous forme d'une fonction Déplacer que vous pouvez annuler.",
"Stand this part up?": "Redresser cette pièce ?",
"Start angle": "Angle de départ",
"Start bulge": "Renflement de début",
"Start continuity": "Continuité de début",
"Step": "Étape",
@@ -411,6 +417,7 @@
"angle, height": "angle, hauteur",
"appearance.dielectric_ior": "Indice de réfraction (diélectrique)",
"appearance.metallic": "Métallique",
"appearance.only_available": "Apparences disponibles uniquement",
"appearance.pbr_help": "Voir cette surface dans l'espace Rendu ou dans un PNG du produit. Sans réfraction ni transmission.",
"appearance.pbr_opaque": "PBR opaque",
"appearance.roughness": "Rugosité",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Insérer un STEP",
"cmd.file_save_mesh": "Enregistrer comme maillage",
"cmd.file_slicers": "Slicers installés",
"cmd.frame_member": "Élément de structure",
"cmd.frame_profile_insert": "Insérer un profilé",
"cmd.frame_profile_list": "Bibliothèque de profilés",
"cmd.frame_profile_properties": "Propriétés du profilé",
"cmd.inspect_accessibility": "Analyse d'accessibilité",
"cmd.inspect_center_of_mass": "Centre de gravité",
"cmd.inspect_clearance": "Jeu",
@@ -568,6 +579,7 @@
"cmd.material_create": "Nouveau matériau physique",
"cmd.material_delete": "Supprimer le matériau physique",
"cmd.material_edit": "Modifier le matériau physique",
"cmd.material_inspect": "Inspecter le matériau physique",
"cmd.material_list": "Lister les matériaux",
"cmd.material_properties_directional": "Module d'élasticité directionnel",
"cmd.material_properties_validate": "Valider les données du matériau physique",
@@ -831,7 +843,7 @@
"dxf.progress.projection": "Projection de la géométrie (3/5)",
"dxf.progress.write": "Écriture du DXF (5/5)",
"dxf.tolerance": "Tolérance des courbes",
"dxf.tolerance_help": "Corps visibles uniquement. Les courbes sont approchées par des segments de droite. Une tolérance plus faible donne des courbes plus fines, au prix de plus de temps et de mémoire.",
"dxf.tolerance_help": "Corps visibles et esquisses affichées uniquement. Les courbes sont approchées par des segments de droite. Une tolérance plus faible donne des courbes plus fines, au prix de plus de temps et de mémoire.",
"file_load.built": "Construit jusqu’ici : {built} entités sur {total}.",
"file_load.converting": "Conversion du solide {done} sur {total}",
"file_load.inserting": "Insertion de la géométrie",
@@ -856,25 +868,69 @@
"help.mit": "Licence MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Coupe les arêtes sélectionnées par un biseau plat de la distance indiquée. À utiliser pour casser des arêtes vives ou faciliter l'entrée d'une goupille ou d'une vis.",
"help.solid.combine": "Joint des corps, les retire d'un corps cible ou ne garde que le volume qu'ils ont en commun, et peut conserver les corps outils. À utiliser pour fusionner des pièces, tailler une forme avec une autre ou repérer où deux corps se recouvrent.",
"help.solid.extrude": "Pousse le profil d'une esquisse en 3D sur une distance, en ajoutant ou en retirant de la matière. À utiliser pour transformer un contour plat en bloc, en bossage ou en poche.",
"help.solid.fillet": "Arrondit les arêtes sélectionnées par un raccord circulaire lisse du rayon indiqué. À utiliser pour adoucir des arêtes vives par souci de solidité, de confort ou d'aspect.",
"help.solid.hole": "Perce un trou rond du diamètre indiqué en un point choisi d'une face. À utiliser pour les passages de vis, les goupilles et autres ouvertures rondes simples.",
"help.solid.loft": "Raccorde en douceur deux profils d'esquisse ou plus, ou un profil et un point, par un solide lissé. À utiliser pour les transitions, entonnoirs, poignées et toute forme dont la section change sur sa longueur.",
"help.solid.mirror": "Crée une copie symétrique des corps ou fonctions sélectionnés par rapport à un plan, et peut la fusionner avec l'original. À utiliser pour les pièces symétriques : on modélise une moitié et on obtient les deux.",
"help.solid.pattern.circular": "Répète les objets sélectionnés autour d'un axe, selon le nombre et l'angle total indiqués. À utiliser pour les cercles de perçages, rayons, ailettes et dents.",
"help.solid.pattern.rectangular": "Répète les objets sélectionnés dans une ou deux directions, selon le nombre et l'espacement indiqués. À utiliser pour les rangées et grilles de trous, nervures, goupilles ou pieds.",
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
"help.solid.sweep": "Entraîne le profil d'une esquisse le long d'une trajectoire pour obtenir un solide. À utiliser pour les tuyaux, rails, poignées et toute pièce de section constante qui suit une courbe.",
"help.third_party": "Licences tierces",
"home.back": "Revenir à {name}",
"home.crashes": "Rapports de plantage",
"home.design_workspace": "Conception",
"home.documentation": "Documentation",
"home.export": "Exporter",
"home.export_hint": "Choisissez un format pour la conception actuelle.",
"home.export_json_hint": "Conception modifiable et historique des commandes",
"home.export_mesh_hint": "Maillage pour l’impression ou le partage",
"home.export_step_hint": "Géométrie solide précise",
"home.import": "Importer…",
"home.import_hint": "STEP, IGES ou maillage dans une nouvelle conception",
"home.in_this_browser": "dans ce navigateur",
"home.missing": "introuvable",
"home.month_01": "janv.",
"home.month_02": "févr.",
"home.month_03": "mars",
"home.month_04": "avr.",
"home.month_05": "mai",
"home.month_06": "juin",
"home.month_07": "juil.",
"home.month_08": "août",
"home.month_09": "sept.",
"home.month_10": "oct.",
"home.month_11": "nov.",
"home.month_12": "déc.",
"home.new_inches": "Nouvelle conception en pouces",
"home.new_inches_hint": "Pouces · ouvre l’espace Conception",
"home.new_mm": "Nouvelle conception",
"home.new_mm_hint": "Millimètres · ouvre l’espace Conception",
"home.open": "Ouvrir…",
"home.opened_days_ago": "Ouvert il y a {count} j",
"home.opened_hours_ago": "Ouvert il y a {count} h",
"home.opened_just_now": "Ouvert à l'instant",
"home.opened_minutes_ago": "Ouvert il y a {count} min",
"home.open_hint": "Une conception .solidcraft ({shortcut})",
"home.open_now": "Ouvrir maintenant",
"home.opened_days_ago": "Il y a {count} j",
"home.opened_hours_ago": "Il y a {count} h",
"home.opened_just_now": "À l’instant",
"home.opened_minutes_ago": "Il y a {count} min",
"home.opening": "ouverture…",
"home.properties": "Propriétés de la conception",
"home.recent": "Récents",
"home.recent_empty": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
"home.recovery_count": "Conceptions récupérables : {count}",
"home.recovery_empty": "Aucune conception récupérable.",
"home.recovery_hint": "SolidCraft s’est fermé avant l’enregistrement de ces conceptions.",
"home.report_issue": "Signaler un problème",
"home.samples": "Exemples",
"home.settings": "Paramètres",
"home.short_date": "{day} {month}",
"home.shortcuts": "Raccourcis clavier",
"home.start_hint": "Créez une conception, ouvrez un fichier récent ou explorez un exemple.",
"home.whats_new": "Nouveautés",
"home.yesterday": "Hier",
"install.button_tip": "Installer SolidCraft comme une app : il s'ouvre dans sa propre fenêtre et fonctionne hors ligne",
"install.dock_1": "Choisissez Fichier › Ajouter au Dock.",
"install.dock_2": "Cliquez sur Ajouter.",
@@ -1005,13 +1061,13 @@
"sample.bracket.name": "Équerre en tôle",
"sample.enclosure.about": "Boîte en coque avec lèvre, bossages de vis, appui et clip",
"sample.enclosure.name": "Boîtier plastique",
"sample.flange.about": "Moyeu et bride avec un réseau circulaire piloté par un paramètre",
"sample.flange.about": "Moyeu et bride avec une répétition circulaire pilotée par un paramètre",
"sample.flange.name": "Bride boulonnée",
"sample.hinge.about": "Deux composants et une liaison pivot, entraînée à 60°",
"sample.hinge.name": "Assemblage de charnière",
"sample.plate.about": "Plaque paramétrique avec congés, perçages et un bossage alésé",
"sample.plate.name": "Plaque d'exemple",
"sample.vents.about": "Rainures en réseau rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.name": "Plaque d'aération",
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
"search.commands": "Commandes",
@@ -1027,12 +1083,14 @@
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
"search.valid": "Valide",
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue. Tant qu'elle est activée, la vue est redessinée en continu.",
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue.",
"settings.measure_axis_distances": "Afficher les cotes X/Y/Z",
"settings.measure_axis_distances_help": "Afficher les cotes suivant les axes du repère global en plus de la distance totale pendant la mesure.",
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
"settings.msaa_reset": "{setting} : {requested}× n'est pas pris en charge par ce périphérique graphique ; la valeur est désormais {reset}× (pris en charge : {supported}).",
"settings.msaa_unsupported": "Non pris en charge par ce périphérique graphique",
"settings.reference_geometry_through_bodies": "Afficher la géométrie de référence à travers les corps",
"settings.reference_geometry_through_bodies_help": "Afficher les contours des esquisses inactives et les plans de référence par-dessus les corps ombrés",
"settings.reset_intro": "Ces réglages ne fonctionnent pas sur cet ordinateur et empêcheraient SolidCraft de démarrer ; ils ont donc été modifiés :",
"settings.reset_title": "Des réglages ont été modifiés",
"settings.supersampling_note": "Dessine la vue à une résolution plus élevée puis la réduit : arêtes, lignes et grille plus lisses sur les écrans de définition inférieure à la 4K. Cela coûte du temps GPU : environ 2,25× plus de pixels à 1,5× et 4× plus à 2×.",
@@ -1088,7 +1146,106 @@
"trace.stopped_by_contact": "Se termine à l'échantillon {sample} : {a} traverserait {b}",
"trace.stopped_by_loop": "Se termine à l'échantillon {sample} : la boucle passant par {joint} ne peut plus se fermer",
"ui.1_degree_of_freedom": "1 degré de liberté",
"ui.3d_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
"ui.3d_mouse_button": "Bouton {number}",
"ui.3d_mouse_button_nothing": "Rien",
"ui.3d_mouse_button_settings": "Réglages de la souris 3D",
"ui.3d_mouse_buttons": "Boutons",
"ui.3d_mouse_buttons_hint": "Appuyez sur un bouton de votre souris 3D pour le trouver ici, puis choisissez ce qu'il fait.",
"ui.3d_mouse_buttons_no_device": "Branchez une souris 3D pour choisir ce que font ses boutons.",
"ui.3d_mouse_buttons_reset": "Rétablir les boutons par défaut",
"ui.3d_mouse_calibrate": "Calibrer…",
"ui.3d_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
"ui.3d_mouse_calibrate_title": "Calibrer la souris 3D",
"ui.3d_mouse_calibrate_wait": "Bougez la souris 3D une fois pour que SolidCraft la voie.",
"ui.3d_mouse_calibrated": "Calibrée",
"ui.3d_mouse_calibration": "Calibrage de la souris 3D",
"ui.3d_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
"ui.3d_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
"ui.3d_mouse_circles_help": "Les cercles montrent ce que l'appareil envoie après calibrage. Le modèle d'essai montre ce que la vue en fait, après inversion, vitesse et affectation des axes.",
"ui.3d_mouse_dead_zone": "Zone morte",
"ui.3d_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
"ui.3d_mouse_denied": "SolidCraft voit une souris 3D mais n'a pas le droit de la lire. La règle udev est dans les notes d'installation de la souris 3D.",
"ui.3d_mouse_denied_short": "Trouvée, mais illisible",
"ui.3d_mouse_driven_by": "Quel mouvement calibré de l'appareil pilote cet axe. Choisir un mouvement déjà utilisé par un autre axe les échange.",
"ui.3d_mouse_driven_by_label": "piloté par",
"ui.3d_mouse_driver": "Pilote de la souris 3D",
"ui.3d_mouse_driver_auto": "Automatique",
"ui.3d_mouse_driver_direct": "Direct (hidraw)",
"ui.3d_mouse_driver_fallback": "Lecture via spacenavd pour l'instant : l'appareil lui-même n'a pas pu être ouvert.",
"ui.3d_mouse_driver_only_direct": "Lecture directe du périphérique ; spacenavd n'est pas lancé",
"ui.3d_mouse_driver_only_spacenavd": "Lecture via spacenavd, seule possibilité disponible",
"ui.3d_mouse_forget": "Oublier le calibrage",
"ui.3d_mouse_ghost_left_viewport": "Vue à gauche",
"ui.3d_mouse_ghost_left_window": "Fenêtre à gauche",
"ui.3d_mouse_ghost_position": "Position",
"ui.3d_mouse_ghost_right_viewport": "Vue à droite",
"ui.3d_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
"ui.3d_mouse_kind_cap": "Retour au centre",
"ui.3d_mouse_kind_question": "Quel type de souris 3D est-ce ?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "La valeur reste tant que vous maintenez, comme sur un appareil de type « Retour au centre ».",
"ui.3d_mouse_looks_like_trackball": "La valeur retombe dès que vous arrêtez de bouger, comme sur un trackball.",
"ui.3d_mouse_motion_rotation_x": "Rotation X",
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
"ui.3d_mouse_motion_translation_x": "Translation X",
"ui.3d_mouse_motion_translation_y": "Translation Y",
"ui.3d_mouse_motion_translation_z": "Translation Z",
"ui.3d_mouse_navigate": "Naviguer avec la souris 3D",
"ui.3d_mouse_none": "Aucune souris 3D trouvée",
"ui.3d_mouse_not_calibrated": "Non calibrée",
"ui.3d_mouse_offer": "Calibrer votre souris 3D corrige les axes échangés ou inversés et règle sa vitesse et sa sensation.",
"ui.3d_mouse_offer_never": "Ne plus afficher",
"ui.3d_mouse_offer_swap": "Passer à Hidraw (pilote direct)",
"ui.3d_mouse_reading": "Lecture",
"ui.3d_mouse_reset_axes": "Réinitialiser l'attribution des axes",
"ui.3d_mouse_reset_view": "Réinitialiser la vue",
"ui.3d_mouse_restore": "Rétablir les réglages de la souris 3D",
"ui.3d_mouse_restore_ask": "Remettre les réglages de la souris 3D par défaut ? Votre calibrage et l'assignation des boutons sont conservés.",
"ui.3d_mouse_section": "Souris 3D",
"ui.3d_mouse_show_in_view": "Afficher la souris 3D dans la vue 3D",
"ui.3d_mouse_show_in_view_tip": "Une image de votre souris 3D dans le coin inférieur gauche, qui bouge quand vous la bougez.",
"ui.3d_mouse_skip": "Mon appareil ne peut pas le faire",
"ui.3d_mouse_skip_tip": "Ignore ce mouvement dans les deux sens ; la calibration le laisse de côté.",
"ui.3d_mouse_speed": "Vitesse de la souris 3D",
"ui.3d_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
"ui.3d_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
"ui.3d_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
"ui.3d_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
"ui.3d_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
"ui.3d_mouse_step_rest_trackball": "Lâchez la boule et laissez-la immobile.",
"ui.3d_mouse_step_ring_clockwise": "Tournez l'anneau de défilement de quelques crans dans le sens horaire.",
"ui.3d_mouse_step_ring_counterclockwise": "Tournez l'anneau de défilement de quelques crans dans le sens antihoraire.",
"ui.3d_mouse_step_roll_away": "Faites rouler la boule loin de vous plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_roll_left": "Faites rouler la boule vers la gauche plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_roll_right": "Faites rouler la boule vers la droite plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_roll_toward": "Faites rouler la boule vers vous plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
"ui.3d_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
"ui.3d_mouse_step_spin_clockwise": "Faites tourner la boule dans le sens horaire plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_spin_counterclockwise": "Faites tourner la boule dans le sens antihoraire plusieurs fois, puis laissez-la s'arrêter.",
"ui.3d_mouse_step_tilt_away": "Inclinez le haut du capuchon loin de vous (le bord le plus proche monte), maintenez, puis relâchez.",
"ui.3d_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
"ui.3d_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
"ui.3d_mouse_step_tilt_toward": "Inclinez le haut du capuchon vers vous (le bord le plus proche descend), maintenez, puis relâchez.",
"ui.3d_mouse_step_touch": "Posez la main sur le capuchon sans le pousser.",
"ui.3d_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
"ui.3d_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
"ui.3d_mouse_switch_cap": "Calibrer comme retour au centre",
"ui.3d_mouse_switch_trackball": "Calibrer comme un trackball",
"ui.3d_mouse_view_axis": "Axe de la vue",
"ui.3d_mouse_view_off": "Désactivé",
"ui.3d_mouse_view_off_tip": "Le profil du pilote de la souris 3D désactive cet axe : son mouvement ne déplace pas la vue. Choisir ici le mouvement d'un autre axe les échange, et ce mouvement cesse de déplacer la vue.",
"ui.3d_mouse_view_orbit_sideways": "Orbite gauche / droite",
"ui.3d_mouse_view_orbit_up_down": "Orbite haut / bas",
"ui.3d_mouse_view_pan_sideways": "Panoramique gauche / droite",
"ui.3d_mouse_view_pan_up_down": "Panoramique haut / bas",
"ui.3d_mouse_view_roll": "Roulis",
"ui.3d_mouse_view_unmapped": "Axe {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "En attente de mouvement",
"ui.3d_mouse_wiggle": "Si vous en avez une de branchée, remuez-la.",
"ui.3d_sketch": "Esquisse 3D",
"ui.activate": "Activer",
"ui.active_component": "Composant actif",
@@ -1098,6 +1255,7 @@
"ui.agent_chat_add": "Joindre des fichiers",
"ui.agent_chat_add_file": "Choisir un fichier…",
"ui.agent_chat_allow": "Autoriser",
"ui.agent_chat_allow_once": "Autoriser une fois",
"ui.agent_chat_allowed": "Autorisé",
"ui.agent_chat_answered": "Réponse : {option}",
"ui.agent_chat_approval": "Autoriser l'agent à effectuer cette action ?",
@@ -1149,12 +1307,16 @@
"ui.agent_chat_files_left_out": "Un message ne peut contenir que {max} fichiers au plus ; non joints : {files}",
"ui.agent_chat_files_loading": "Lecture des fichiers en cours",
"ui.agent_chat_files_max": "{max} fichiers au plus par message",
"ui.agent_chat_flatpak_none_found": "Aucun agent de programmation trouvé. Par défaut, le bac à sable Flatpak ne voit pas les agents installés dans votre dossier personnel et n’a pas accès au réseau. Pour autoriser Claude Code et son adaptateur ACP installés avec nvm ou dans ~/.local/bin, exécuter cette commande dans un terminal, puis redémarrer SolidCraft. Elle donne accès aux programmes et fichiers de connexion indiqués, ainsi qu’au réseau :",
"ui.agent_chat_import_failed": "{name} : l'importation a échoué : {error}",
"ui.agent_chat_intro": "Discuter avec un agent de programmation que vous utilisez déjà, ou avec un point d'accès de modèle de votre choix. Il travaille sur la conception ouverte avec les outils de SolidCraft : ses modifications apparaissent au fur et à mesure et s'annulent en une seule étape par message. Ce qu'il peut faire sans demander se règle dans Préférences › IA.",
"ui.agent_chat_key_in_env": "clé définie",
"ui.agent_chat_left": "Gauche",
"ui.agent_chat_limit": "L'agent a atteint la limite de {limit} modifications pour un message. Envoyez un autre message pour qu'il poursuive.",
"ui.agent_chat_minimize": "Réduire en bandeau",
"ui.agent_chat_mode_all": "Tout",
"ui.agent_chat_mode_ask": "Demander",
"ui.agent_chat_mode_edits": "Édition",
"ui.agent_chat_model_tip": "Le modèle utilisé par l'agent. Votre choix est mémorisé pour cet agent.",
"ui.agent_chat_new": "Nouveau chat",
"ui.agent_chat_new_confirm": "Démarrer un nouveau chat ? Le chat actuel sera effacé.",
@@ -1167,6 +1329,7 @@
"ui.agent_chat_none_found_short": "Aucun agent trouvé",
"ui.agent_chat_not_installed": "Non installé",
"ui.agent_chat_not_signed_in": "non authentifié",
"ui.agent_chat_open_settings": "Ouvrir les préférences",
"ui.agent_chat_own_window": "Fenêtre séparée",
"ui.agent_chat_paste_picture": "Coller une image",
"ui.agent_chat_permission": "L'agent demande à exécuter : {title}",
@@ -1191,6 +1354,8 @@
"ui.agent_chat_send": "Envoyer",
"ui.agent_chat_send_now": "Envoyer maintenant",
"ui.agent_chat_send_now_tip": "Arrêter l'étape en cours de l'agent et envoyer celui-ci ensuite (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft communique avec un agent de programmation installé sur cet ordinateur. Choisissez-en un dans Préférences › IA.",
"ui.agent_chat_setup_title": "Configurer un assistant",
"ui.agent_chat_side": "Côté du chat de l'agent",
"ui.agent_chat_signed_in": "authentifié",
"ui.agent_chat_state_auth": "Authentification requise",
@@ -1207,6 +1372,7 @@
"ui.agent_chat_undo_label": "Chat de l'agent : {message}",
"ui.agent_chat_undone": "(annulé)",
"ui.agent_chat_view_attached": "Vue jointe",
"ui.agent_chat_waiting": "En attente de votre réponse",
"ui.agent_cursor": "Curseur de l'agent",
"ui.agent_cursor_speed": "Vitesse du curseur de l'agent",
"ui.ai_add": "Ajouter",
@@ -1264,7 +1430,6 @@
"ui.ai_permissions": "Autorisations",
"ui.ai_preset": "Préréglage",
"ui.ai_remove": "Retirer",
"ui.ai_request_limit": "arrêté après 24 requêtes pour un message ; envoyez un autre message pour poursuivre",
"ui.ai_reset": "Réinitialiser",
"ui.ai_settings": "Réglages IA…",
"ui.ai_show_features": "Afficher les fonctionnalités IA",
@@ -1309,6 +1474,7 @@
"ui.blocked": "Inaccessible",
"ui.bodies": "Corps",
"ui.bodies_all_if_none": "Corps (tous si aucun)",
"ui.bottom": "Dessous",
"ui.box": "Boîte",
"ui.break": "Fractionner",
"ui.browser_bodies_one": "{count} corps",
@@ -1326,7 +1492,7 @@
"ui.chamfer_type": "Type de chanfrein",
"ui.change_workspace": "Changer d'espace de travail",
"ui.circle": "Cercle",
"ui.circular_pattern": "Réseau circulaire",
"ui.circular_pattern": "Répétition circulaire",
"ui.clear": "Effacer",
"ui.clear_recent": "Effacer les récents",
"ui.clear_the_selection": "Effacer la sélection",
@@ -1461,11 +1627,12 @@
"ui.grid_follows_view_hint": "Dans les vues de côté, la grille s'affiche sur le plan vertical qui leur fait face plutôt que sur le sol",
"ui.ground_shadow": "Ombre au sol",
"ui.groupSelected": "Grouper la sélection",
"ui.hatch": "Hachures",
"ui.heading_add_ins": "COMPLÉMENTS",
"ui.heading_assemble": "ASSEMBLER",
"ui.heading_box": "BOÎTE",
"ui.heading_chamfer": "CHANFREIN",
"ui.heading_circular_pattern": "RÉSEAU CIRCULAIRE",
"ui.heading_circular_pattern": "RÉPÉTITION CIRCULAIRE",
"ui.heading_coil": "HÉLICE",
"ui.heading_combine": "COMBINER",
"ui.heading_configure": "CONFIGURER",
@@ -1498,14 +1665,14 @@
"ui.heading_offset_plane": "PLAN DÉCALÉ",
"ui.heading_ok": "OK",
"ui.heading_parameters": "PARAMÈTRES",
"ui.heading_pattern": "RÉSEAU",
"ui.heading_pattern_on_path": "RÉSEAU SUR TRAJECTOIRE",
"ui.heading_pattern": "RÉPÉTITION",
"ui.heading_pattern_on_path": "RÉPÉTITION SUR TRAJECTOIRE",
"ui.heading_pipe": "TUYAU",
"ui.heading_plane_at_angle": "PLAN INCLINÉ",
"ui.heading_plastic": "PLASTIQUE",
"ui.heading_position": "POSITION",
"ui.heading_prepare": "PRÉPARER",
"ui.heading_rectangular_pattern": "RÉSEAU RECTANGULAIRE",
"ui.heading_rectangular_pattern": "RÉPÉTITION RECTANGULAIRE",
"ui.heading_revolve": "RÉVOLUTION",
"ui.heading_right": "DROITE",
"ui.heading_save_mesh": "ENREGISTRER COMME MAILLAGE",
@@ -1534,6 +1701,11 @@
"ui.hideOrigin": "Masquer l'origine",
"ui.hidePlanes": "Masquer les plans",
"ui.hideProfile": "Masquer le profil",
"ui.history_back": "Reculer d’une étape",
"ui.history_below_browser": "Sous le navigateur",
"ui.history_first": "Aller au début",
"ui.history_forward": "Avancer d’une étape",
"ui.history_last": "Aller à la fin",
"ui.history_title": "Historique",
"ui.hole": "Perçage",
"ui.hole_tap_type": "Type de taraudage",
@@ -1551,6 +1723,8 @@
"ui.joints": "Liaisons",
"ui.keyboard_shortcuts": "Raccourcis clavier",
"ui.language": "Langue",
"ui.layout_compact": "Compacte",
"ui.layout_floating": "Flottante",
"ui.length_decimals": "Décimales des longueurs",
"ui.light": "Clair",
"ui.line": "Ligne",
@@ -1628,7 +1802,7 @@
"ui.part_size_shared": "Reconstruit la pièce en remplaçant les entités ajoutées. Toutes les occurrences de ce composant sont mises à jour.",
"ui.paste": "Coller",
"ui.path": "Trajectoire",
"ui.pattern_on_path": "Réseau sur trajectoire",
"ui.pattern_on_path": "Répétition sur trajectoire",
"ui.perpendicular": "Perpendicularité",
"ui.perspective_view": "Vue en perspective",
"ui.pinToShortcuts": "Épingler aux raccourcis",
@@ -1665,7 +1839,7 @@
"ui.rail_fit": "Ajustement au rail",
"ui.reachable": "Accessible",
"ui.rectangle": "Rectangle",
"ui.rectangular_pattern": "Réseau rectangulaire",
"ui.rectangular_pattern": "Répétition rectangulaire",
"ui.redo": "Rétablir",
"ui.relink_design_changed": "L’assemblage a changé pendant le choix d’une source. Relancez Relier à nouveau la source.",
"ui.relink_done": "Source reliée à nouveau pour toutes les instances.",
@@ -1688,6 +1862,7 @@
"ui.reverse_zoom_direction": "Inverser le sens du zoom",
"ui.revolve": "Révolution",
"ui.ribbon_exit": "Quitter",
"ui.ribbon_labels": "Afficher le texte sous les icônes du ruban",
"ui.ribbon_pin_footer": "Les commandes épinglées s’affichent dans le ruban. Survolez une ligne pour l’épingler ou la détacher.",
"ui.ribbon_workspace": "Espace de travail",
"ui.right": "Droite",
@@ -1743,34 +1918,6 @@
"ui.smooth_wheel_zoom": "Zoom fluide à la molette",
"ui.smooth_wheel_zoom_tip": "Désactiver pour appliquer chaque cran de molette immédiatement, sans lissage",
"ui.snap": "Accrochage",
"ui.space_mouse_calibrate": "Calibrer…",
"ui.space_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
"ui.space_mouse_calibrate_title": "Calibrer la souris 3D",
"ui.space_mouse_calibrated": "Calibrée",
"ui.space_mouse_calibration": "Calibrage de la souris 3D",
"ui.space_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
"ui.space_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
"ui.space_mouse_dead_zone": "Zone morte",
"ui.space_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
"ui.space_mouse_forget": "Oublier le calibrage",
"ui.space_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
"ui.space_mouse_navigate": "Naviguer avec SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Aucune souris 3D trouvée",
"ui.space_mouse_not_calibrated": "Non calibrée",
"ui.space_mouse_reading": "Lecture",
"ui.space_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
"ui.space_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
"ui.space_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
"ui.space_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
"ui.space_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
"ui.space_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
"ui.space_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
"ui.space_mouse_step_tilt_back": "Inclinez le capuchon vers l'arrière (bord avant vers le haut), maintenez, puis relâchez.",
"ui.space_mouse_step_tilt_forward": "Inclinez le capuchon vers l'avant (bord avant vers le bas), maintenez, puis relâchez.",
"ui.space_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
"ui.space_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
"ui.space_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
"ui.space_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
"ui.spacing": "Espacement",
"ui.spacing_2": "Espacement 2",
"ui.sphere": "Sphère",
@@ -1842,6 +1989,7 @@
"ui.workspace_design_tabs": "Modèle · Surface · Assemblage · Insérer · Inspecter · Gérer · Vue",
"ui.workspace_drawing_description": "Mises en plan 2D avec vues, cotes et cartouches",
"ui.workspace_footer": "Chaque espace de travail s’applique à la même conception, au même historique et à la même sélection ; seuls les onglets du ruban changent. Le sélecteur est verrouillé pendant la modification d’une esquisse ou d’une entité.",
"ui.workspace_layout": "Disposition de l’espace de travail",
"ui.workspace_manufacture_description": "Tranchage, parcours d’outil et configuration",
"ui.workspace_planned": "Prévu",
"ui.workspace_render_description": "Aperçu du produit, paramètres de scène et images rendues",
+176 -36
View File
@@ -72,6 +72,7 @@
"Choose…": "Escoller…",
"Circular": "Circular",
"Circumference": "Circunferencia",
"Clockwise": "Horario",
"Close draft": "Pechar o borrador",
"Collisions": "Colisións",
"Colour": "Cor",
@@ -167,6 +168,7 @@
"HISTORY": "HISTORIAL",
"Height": "Altura",
"Hidden edges": "Arestas ocultas",
"History layout": "Disposición do historial",
"Holes": "Furados",
"Holes (edges on them)": "Furados (arestas sobre eles)",
"Home view": "Vista de inicio",
@@ -232,7 +234,7 @@
"No crash reports.": "Non hai informes de fallos.",
"No matching appearance presets.": "Non hai preaxustes de aparencia coincidentes.",
"No matching appearances": "Non hai aparencias coincidentes",
"No visible bodies to export.": "Non hai corpos visibles para exportar.",
"No visible bodies or sketches to export.": "Non hai corpos nin bosquexos visibles para exportar.",
"None": "Ningún",
"Not available yet": "Aínda non dispoñible",
"Notch": "Entalle",
@@ -321,6 +323,7 @@
"Search commands": "Buscar ordes",
"Search this library": "Buscar nesta biblioteca",
"Section": "Sección",
"Section position": "Posición da sección",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar outro: mantén o botón esquerdo quieto sobre o modelo (ou clic dereito › Seleccionar outro…) para listar todo o que hai baixo o cursor; fai clic de novo no mesmo punto para seleccionar o seguinte. Fai dobre clic co botón central para ir ao Inicio (Preferencias › Navegación).",
"Select bodies (a face picks its body)": "Selecciona corpos (unha cara escolle o seu corpo)",
"Select bodies or faces instead": "Seleccionar corpos ou caras no seu lugar",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Mostrar o cursor do axente",
"Show in folder": "Mostrar no cartafol",
"Show the pointer an AI agent moves (#35)": "Mostrar o punteiro que move un axente de IA (#35)",
"Show tool at cursor": "Mostrar a ferramenta xunto ao cursor",
"Signed axes": "Eixes con signo",
"Size": "Tamaño",
"Sketch constraints": "Restricións do bosquexo",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft pechouse antes de gardar estes deseños. Pódese abrir a súa última gravación automática.",
"Specialise": "Especializar",
"Speed": "Velocidade",
"Square": "Cadrado",
"Stand it up": "Poñela de pé",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Poñela de pé: un cuarto de volta arredor de X, como unha operación Mover que podes desfacer.",
"Stand this part up?": "Poñer esta peza de pé?",
"Start angle": "Ángulo inicial",
"Start bulge": "Abaulamento inicial",
"Start continuity": "Continuidade inicial",
"Step": "Paso",
@@ -411,6 +417,7 @@
"angle, height": "ángulo, altura",
"appearance.dielectric_ior": "IOR dieléctrico",
"appearance.metallic": "Metálico",
"appearance.only_available": "Só aparencias dispoñibles",
"appearance.pbr_help": "Ve esta superficie en Render ou nun PNG de produto. Sen refracción nin transmisión.",
"appearance.pbr_opaque": "PBR opaco",
"appearance.roughness": "Rugosidade",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Inserir STEP",
"cmd.file_save_mesh": "Gardar como malla",
"cmd.file_slicers": "Laminadores instalados",
"cmd.frame_member": "Membro estrutural",
"cmd.frame_profile_insert": "Inserir perfil",
"cmd.frame_profile_list": "Biblioteca de perfís",
"cmd.frame_profile_properties": "Propiedades do perfil",
"cmd.inspect_accessibility": "Análise de accesibilidade",
"cmd.inspect_center_of_mass": "Centro de masas",
"cmd.inspect_clearance": "Folga",
@@ -568,6 +579,7 @@
"cmd.material_create": "Material físico novo",
"cmd.material_delete": "Eliminar o material físico",
"cmd.material_edit": "Editar o material físico",
"cmd.material_inspect": "Inspeccionar o material físico",
"cmd.material_list": "Listar os materiais",
"cmd.material_properties_directional": "Módulo elástico direccional",
"cmd.material_properties_validate": "Validar os datos do material físico",
@@ -831,7 +843,7 @@
"dxf.progress.projection": "Proxectando a xeometría (3/5)",
"dxf.progress.write": "Escribindo o DXF (5/5)",
"dxf.tolerance": "Tolerancia da curva",
"dxf.tolerance_help": "Só os corpos visibles. As curvas usan segmentos rectos. Unha tolerancia menor dá curvas máis finas e usa máis tempo e memoria.",
"dxf.tolerance_help": "Só os corpos visibles e os bosquexos amosados. As curvas usan segmentos rectos. Unha tolerancia menor dá curvas máis finas e usa máis tempo e memoria.",
"file_load.built": "Construído ata agora: {built} de {total} operacións.",
"file_load.converting": "Convertendo o sólido {done} de {total}",
"file_load.inserting": "Inserindo a xeometría",
@@ -856,19 +868,57 @@
"help.mit": "Licenza MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenzas de terceiros",
"home.back": "Volver a {name}",
"home.crashes": "Informes de fallos",
"home.design_workspace": "Deseño",
"home.documentation": "Documentación",
"home.export": "Exportar",
"home.export_hint": "Escolle un formato para o deseño actual.",
"home.export_json_hint": "Deseño editable e historial de comandos",
"home.export_mesh_hint": "Malla para imprimir ou compartir",
"home.export_step_hint": "Xeometría sólida precisa",
"home.import": "Importar…",
"home.import_hint": "STEP, IGES ou malla nun novo deseño",
"home.in_this_browser": "neste navegador",
"home.missing": "non atopado",
"home.month_01": "xan.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "abr.",
"home.month_05": "maio",
"home.month_06": "xuño",
"home.month_07": "xul.",
"home.month_08": "ago.",
"home.month_09": "set.",
"home.month_10": "out.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Novo deseño en polgadas",
"home.new_inches_hint": "Polgadas · ábrese en Deseño",
"home.new_mm": "Novo deseño",
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
"home.open": "Abrir…",
"home.opened_days_ago": "Aberto hai {count} días",
"home.opened_hours_ago": "Aberto hai {count} h",
"home.opened_just_now": "Aberto agora mesmo",
"home.opened_minutes_ago": "Aberto hai {count} min",
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Hai {count} días",
"home.opened_hours_ago": "Hai {count} h",
"home.opened_just_now": "Agora mesmo",
"home.opened_minutes_ago": "Hai {count} min",
"home.opening": "abrindo…",
"home.properties": "Propiedades do deseño",
"home.recent": "Recentes",
"home.recent_empty": "Os deseños que abras ou gardes aparecerán aquí.",
"home.recovery_count": "Deseños recuperables: {count}",
"home.recovery_empty": "Non hai deseños recuperables.",
"home.recovery_hint": "SolidCraft pechouse antes de gardar estes deseños.",
"home.report_issue": "Informar dunha incidencia",
"home.samples": "Exemplos",
"home.settings": "Configuración",
"home.short_date": "{day} {month}",
"home.shortcuts": "Atallos de teclado",
"home.start_hint": "Crea un deseño, abre un ficheiro recente ou explora un exemplo.",
"home.whats_new": "Novidades",
"home.yesterday": "Onte",
"install.button_tip": "Instala SolidCraft como aplicación: ábrese na súa propia xanela e funciona sen conexión",
"install.dock_1": "Escolle Ficheiro › Engadir ao Dock.",
"install.dock_2": "Fai clic en Engadir.",
@@ -1021,7 +1071,7 @@
"search.try": "Escribe o nome dunha orde, unha operación deste deseño ou un parámetro. Para executar unha orde con valores, escribe o seu id e un obxecto JSON: {example}",
"search.valid": "Válido",
"settings.display_quality_note": "Os niveis máis finos debuxan as caras e arestas curvas máis suaves e tardan máis en xerar a malla. Só cambia a vista, nunca o deseño nin as exportacións.",
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista. Mentres está activado, a vista redebúxase continuamente.",
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista.",
"settings.measure_axis_distances": "Mostrar as distancias en X/Y/Z",
"settings.measure_axis_distances_help": "Mostra as compoñentes da distancia segundo os eixes do mundo xunto á distancia total mentres Medir está aberto.",
"settings.msaa_applies_on_restart": "O antialiasing aplicarase a próxima vez que se inicie SolidCraft",
@@ -1082,7 +1132,106 @@
"trace.stopped_by_contact": "Remata na mostra {sample}: {a} atravesaría {b}",
"trace.stopped_by_loop": "Remata na mostra {sample}: o bucle a través de {joint} non pode pecharse máis",
"ui.1_degree_of_freedom": "1 grao de liberdade",
"ui.3d_mouse_axes_reassigned": "Os eixes están reasignados en Preferencias, e iso segue aplicándose sobre esta calibración.",
"ui.3d_mouse_button": "Botón {number}",
"ui.3d_mouse_button_nothing": "Nada",
"ui.3d_mouse_button_settings": "Axustes do rato 3D",
"ui.3d_mouse_buttons": "Botóns",
"ui.3d_mouse_buttons_hint": "Preme un botón do teu rato 3D para atopalo aquí e escolle que fai.",
"ui.3d_mouse_buttons_no_device": "Conecta un rato 3D para escoller que fan os seus botóns.",
"ui.3d_mouse_buttons_reset": "Restaurar os botóns predeterminados",
"ui.3d_mouse_calibrate": "Calibrar…",
"ui.3d_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
"ui.3d_mouse_calibrate_title": "Calibrar o rato 3D",
"ui.3d_mouse_calibrate_wait": "Move o rato 3D unha vez para que SolidCraft o vexa.",
"ui.3d_mouse_calibrated": "Calibrado",
"ui.3d_mouse_calibration": "Calibración do rato 3D",
"ui.3d_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
"ui.3d_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
"ui.3d_mouse_circles_help": "Os círculos mostran o que envía o dispositivo tras a calibración. O modelo de proba mostra o que fai a vista con iso, tras a inversión, a velocidade e a asignación de eixes.",
"ui.3d_mouse_dead_zone": "Zona morta",
"ui.3d_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
"ui.3d_mouse_denied": "SolidCraft ve un rato 3D, pero non ten permiso para lelo. A regra udev está nas notas de configuración do rato 3D.",
"ui.3d_mouse_denied_short": "Atopado, pero non se pode ler",
"ui.3d_mouse_driven_by": "Que movemento calibrado do dispositivo controla este eixe. Se escolles un que xa usa outro eixe, intercámbianse.",
"ui.3d_mouse_driven_by_label": "controlado por",
"ui.3d_mouse_driver": "Controlador do rato 3D",
"ui.3d_mouse_driver_auto": "Automático",
"ui.3d_mouse_driver_direct": "Directo (hidraw)",
"ui.3d_mouse_driver_fallback": "Lendo polo de agora a través de spacenavd: non se puido abrir o dispositivo.",
"ui.3d_mouse_driver_only_direct": "Lectura directa do dispositivo; spacenavd non está en execución",
"ui.3d_mouse_driver_only_spacenavd": "Lectura mediante spacenavd, a única opción dispoñible",
"ui.3d_mouse_forget": "Esquecer a calibración",
"ui.3d_mouse_ghost_left_viewport": "Vista esquerda",
"ui.3d_mouse_ghost_left_window": "Xanela esquerda",
"ui.3d_mouse_ghost_position": "Posición",
"ui.3d_mouse_ghost_right_viewport": "Vista dereita",
"ui.3d_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
"ui.3d_mouse_kind_cap": "Retorno ao centro",
"ui.3d_mouse_kind_question": "Que tipo de rato 3D é?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "A lectura mantense mentres a suxeitas, como nun dispositivo de tipo «Retorno ao centro».",
"ui.3d_mouse_looks_like_trackball": "A lectura volve atrás en canto deixas de moverte, como nun trackball.",
"ui.3d_mouse_motion_rotation_x": "Rotación X",
"ui.3d_mouse_motion_rotation_y": "Rotación Y",
"ui.3d_mouse_motion_rotation_z": "Rotación Z",
"ui.3d_mouse_motion_translation_x": "Translación X",
"ui.3d_mouse_motion_translation_y": "Translación Y",
"ui.3d_mouse_motion_translation_z": "Translación Z",
"ui.3d_mouse_navigate": "Navegar co rato 3D",
"ui.3d_mouse_none": "Non se atopou ningún rato 3D",
"ui.3d_mouse_not_calibrated": "Sen calibrar",
"ui.3d_mouse_offer": "Calibrar o teu rato 3D corrixe os eixes trocados ou invertidos e axusta a súa velocidade e sensación.",
"ui.3d_mouse_offer_never": "Non mostrar de novo",
"ui.3d_mouse_offer_swap": "Cambiar a Hidraw (controlador directo)",
"ui.3d_mouse_reading": "Lectura",
"ui.3d_mouse_reset_axes": "Restablecer asignación de eixes",
"ui.3d_mouse_reset_view": "Restablecer vista",
"ui.3d_mouse_restore": "Restaurar valores do rato 3D",
"ui.3d_mouse_restore_ask": "Volver aos valores predeterminados da configuración do rato 3D? A calibración e a asignación de botóns consérvanse.",
"ui.3d_mouse_section": "Rato 3D",
"ui.3d_mouse_show_in_view": "Mostrar o rato 3D na vista 3D",
"ui.3d_mouse_show_in_view_tip": "Unha imaxe do teu rato 3D na esquina inferior esquerda que se move cando o moves.",
"ui.3d_mouse_skip": "O meu dispositivo non fai isto",
"ui.3d_mouse_skip_tip": "Omite este movemento nos dous sentidos; a calibración déixao fóra.",
"ui.3d_mouse_speed": "Velocidade do rato 3D",
"ui.3d_mouse_step_lift": "Levanta o capuchón, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_press": "Preme o capuchón cara abaixo, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_pull": "Tira do capuchón cara a ti, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_push": "Empurra o capuchón lonxe de ti, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_rest": "Solta o capuchón e déixao quieto.",
"ui.3d_mouse_step_rest_trackball": "Solta a bóla e déixaa quieta.",
"ui.3d_mouse_step_ring_clockwise": "Xira o anel de desprazamento uns clics no sentido horario.",
"ui.3d_mouse_step_ring_counterclockwise": "Xira o anel de desprazamento uns clics no sentido antihorario.",
"ui.3d_mouse_step_roll_away": "Fai rodar a bóla lonxe de ti unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_roll_left": "Fai rodar a bóla á esquerda unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_roll_right": "Fai rodar a bóla á dereita unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_roll_toward": "Fai rodar a bóla cara a ti unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_slide_right": "Despraza o capuchón á dereita, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_spin_clockwise": "Xira a bóla no sentido horario unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_spin_counterclockwise": "Xira a bóla no sentido antihorario unhas cantas veces e déixaa parar.",
"ui.3d_mouse_step_tilt_away": "Inclina a parte de arriba do capuchón lonxe de ti (o bordo máis próximo sobe), mantén a posición e despois sóltao.",
"ui.3d_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantén a posición e despois sóltao.",
"ui.3d_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantén a posición e despois sóltao.",
"ui.3d_mouse_step_tilt_toward": "Inclina a parte de arriba do capuchón cara a ti (o bordo máis próximo baixa), mantén a posición e despois sóltao.",
"ui.3d_mouse_step_touch": "Apoia a man no capuchón sen empurralo.",
"ui.3d_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantén a posición e despois sóltao.",
"ui.3d_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantén a posición e despois sóltao.",
"ui.3d_mouse_switch_cap": "Calibrar como retorno ao centro",
"ui.3d_mouse_switch_trackball": "Calibrar como trackball",
"ui.3d_mouse_view_axis": "Eixe da vista",
"ui.3d_mouse_view_off": "Desactivado",
"ui.3d_mouse_view_off_tip": "O perfil do controlador do rato 3D desactiva este eixe, así que o seu movemento non move a vista. Se escolles aquí o movemento doutro eixe, intercámbianse e ese movemento deixa de mover a vista.",
"ui.3d_mouse_view_orbit_sideways": "Orbitar á esquerda / dereita",
"ui.3d_mouse_view_orbit_up_down": "Orbitar arriba / abaixo",
"ui.3d_mouse_view_pan_sideways": "Desprazar á esquerda / dereita",
"ui.3d_mouse_view_pan_up_down": "Desprazar arriba / abaixo",
"ui.3d_mouse_view_roll": "Xirar sobre a vista",
"ui.3d_mouse_view_unmapped": "Eixe {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "Agardando movemento",
"ui.3d_mouse_wiggle": "Se tes un conectado, móveo un pouco.",
"ui.3d_sketch": "Bosquexo 3D",
"ui.activate": "Activar",
"ui.active_component": "Compoñente activo",
@@ -1092,6 +1241,7 @@
"ui.agent_chat_add": "Anexar ficheiros",
"ui.agent_chat_add_file": "Escoller un ficheiro…",
"ui.agent_chat_allow": "Permitir",
"ui.agent_chat_allow_once": "Permitir unha vez",
"ui.agent_chat_allowed": "Permitido",
"ui.agent_chat_answered": "Resposta: {option}",
"ui.agent_chat_approval": "Permitir que o axente faga isto?",
@@ -1143,12 +1293,16 @@
"ui.agent_chat_files_left_out": "Cunha mensaxe van como máximo {max} ficheiros; deixados fóra: {files}",
"ui.agent_chat_files_loading": "Agardando a que se lean os ficheiros",
"ui.agent_chat_files_max": "Como máximo {max} ficheiros por mensaxe",
"ui.agent_chat_flatpak_none_found": "Non se atopou ningún axente de programación. De forma predeterminada, o illamento de Flatpak non ve os axentes instalados no teu cartafol persoal e non ten acceso á rede. Para permitir Claude Code e o seu adaptador ACP instalados con nvm ou en ~/.local/bin, executa esta orde nun terminal e despois reinicia SolidCraft. Concede acceso aos programas e ficheiros de inicio de sesión indicados, ademais de acceso á rede:",
"ui.agent_chat_import_failed": "{name}: fallou a importación: {error}",
"ui.agent_chat_intro": "Conversa cun axente de programación que xa uses, ou cun punto de acceso de modelo que escollas. Traballa no deseño aberto a través das ferramentas de SolidCraft: os seus cambios vense mentres os fai e desfanse nun paso por mensaxe. O que pode facer sen preguntar defínese en Preferencias > IA.",
"ui.agent_chat_key_in_env": "clave configurada",
"ui.agent_chat_left": "Esquerda",
"ui.agent_chat_limit": "O axente chegou ao límite de {limit} cambios por mensaxe. Envía outra mensaxe para que continúe.",
"ui.agent_chat_minimize": "Minimizar nunha franxa",
"ui.agent_chat_mode_all": "Todo",
"ui.agent_chat_mode_ask": "Preguntar",
"ui.agent_chat_mode_edits": "Edicións",
"ui.agent_chat_model_tip": "O modelo que usa o axente. A túa escolla lémbrase para este axente.",
"ui.agent_chat_new": "Chat novo",
"ui.agent_chat_new_confirm": "Comezar un chat novo? Borrarase o actual.",
@@ -1161,6 +1315,7 @@
"ui.agent_chat_none_found_short": "Non se atopou ningún axente",
"ui.agent_chat_not_installed": "Non instalado",
"ui.agent_chat_not_signed_in": "sen sesión iniciada",
"ui.agent_chat_open_settings": "Abrir preferencias",
"ui.agent_chat_own_window": "Xanela propia",
"ui.agent_chat_paste_picture": "Pegar unha imaxe",
"ui.agent_chat_permission": "O axente pide executar: {title}",
@@ -1185,6 +1340,8 @@
"ui.agent_chat_send": "Enviar",
"ui.agent_chat_send_now": "Enviar agora",
"ui.agent_chat_send_now_tip": "Detén o paso actual do axente e envía isto a continuación (Ctrl+Intro)",
"ui.agent_chat_setup_body": "SolidCraft comunícase cun axente de programación instalado neste equipo. Escolle un en Preferencias › IA.",
"ui.agent_chat_setup_title": "Configurar un asistente",
"ui.agent_chat_side": "Lado do chat co axente",
"ui.agent_chat_signed_in": "sesión iniciada",
"ui.agent_chat_state_auth": "Hai que iniciar sesión",
@@ -1201,6 +1358,7 @@
"ui.agent_chat_undo_label": "Chat co axente: {message}",
"ui.agent_chat_undone": "(desfeito)",
"ui.agent_chat_view_attached": "Vista achegada",
"ui.agent_chat_waiting": "Agardando a túa resposta",
"ui.agent_cursor": "Cursor do axente",
"ui.agent_cursor_speed": "Velocidade do cursor do axente",
"ui.ai_add": "Engadir",
@@ -1258,7 +1416,6 @@
"ui.ai_permissions": "Permisos",
"ui.ai_preset": "Axuste predefinido",
"ui.ai_remove": "Quitar",
"ui.ai_request_limit": "detívose tras 24 solicitudes para unha mensaxe; envía outra mensaxe para continuar",
"ui.ai_reset": "Restablecer",
"ui.ai_settings": "Axustes de IA…",
"ui.ai_show_features": "Mostrar as funcións de IA",
@@ -1303,6 +1460,7 @@
"ui.blocked": "Bloqueado",
"ui.bodies": "Corpos",
"ui.bodies_all_if_none": "Corpos (todos se non hai ningún)",
"ui.bottom": "Inferior",
"ui.box": "Caixa",
"ui.break": "Romper",
"ui.browser_bodies_one": "{count} corpo",
@@ -1455,6 +1613,7 @@
"ui.grid_follows_view_hint": "As vistas laterais mostran a grella no plano vertical que ten diante en vez do chan",
"ui.ground_shadow": "Sombra no chan",
"ui.groupSelected": "Agrupar a selección",
"ui.hatch": "Raiado",
"ui.heading_add_ins": "COMPLEMENTOS",
"ui.heading_assemble": "ENSAMBLAR",
"ui.heading_box": "CAIXA",
@@ -1528,6 +1687,11 @@
"ui.hideOrigin": "Ocultar a orixe",
"ui.hidePlanes": "Ocultar os planos",
"ui.hideProfile": "Ocultar o perfil",
"ui.history_back": "Retroceder un paso",
"ui.history_below_browser": "Debaixo do explorador",
"ui.history_first": "Ir ao comezo",
"ui.history_forward": "Avanzar un paso",
"ui.history_last": "Ir ao final",
"ui.history_title": "Historial",
"ui.hole": "Furado",
"ui.hole_tap_type": "Tipo de roscado do furado",
@@ -1545,6 +1709,8 @@
"ui.joints": "Unións",
"ui.keyboard_shortcuts": "Atallos de teclado",
"ui.language": "Idioma",
"ui.layout_compact": "Compacta",
"ui.layout_floating": "Flotante",
"ui.length_decimals": "Decimais das lonxitudes",
"ui.light": "Claro",
"ui.line": "Liña",
@@ -1682,6 +1848,7 @@
"ui.reverse_zoom_direction": "Inverter a dirección do zoom",
"ui.revolve": "Revolución",
"ui.ribbon_exit": "Saír",
"ui.ribbon_labels": "Mostrar texto baixo as iconas da cinta",
"ui.ribbon_pin_footer": "As ordes fixadas móstranse na cinta. Pasa o cursor sobre unha fila para fixala ou soltala.",
"ui.ribbon_workspace": "Espazo de traballo",
"ui.right": "Dereita",
@@ -1737,34 +1904,6 @@
"ui.smooth_wheel_zoom": "Zoom suave coa roda",
"ui.smooth_wheel_zoom_tip": "Desactívao para aplicar cada movemento da roda de inmediato, sen suavizado",
"ui.snap": "Axuste",
"ui.space_mouse_calibrate": "Calibrar…",
"ui.space_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
"ui.space_mouse_calibrate_title": "Calibrar o rato 3D",
"ui.space_mouse_calibrated": "Calibrado",
"ui.space_mouse_calibration": "Calibración do rato 3D",
"ui.space_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
"ui.space_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
"ui.space_mouse_dead_zone": "Zona morta",
"ui.space_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
"ui.space_mouse_forget": "Esquecer a calibración",
"ui.space_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
"ui.space_mouse_navigate": "Navegar con SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Non se atopou ningún rato 3D",
"ui.space_mouse_not_calibrated": "Sen calibrar",
"ui.space_mouse_reading": "Lectura",
"ui.space_mouse_step_lift": "Levanta o capuchón, mantéño e sóltao.",
"ui.space_mouse_step_press": "Preme o capuchón cara abaixo, mantéño e sóltao.",
"ui.space_mouse_step_pull": "Tira do capuchón cara a ti, mantéño e sóltao.",
"ui.space_mouse_step_push": "Empurra o capuchón lonxe de ti, mantéño e sóltao.",
"ui.space_mouse_step_rest": "Solta o capuchón e déixao quieto.",
"ui.space_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantéño e sóltao.",
"ui.space_mouse_step_slide_right": "Despraza o capuchón á dereita, mantéño e sóltao.",
"ui.space_mouse_step_tilt_back": "Inclina o capuchón cara atrás (bordo dianteiro arriba), mantéño e sóltao.",
"ui.space_mouse_step_tilt_forward": "Inclina o capuchón cara adiante (bordo dianteiro abaixo), mantéño e sóltao.",
"ui.space_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantéño e sóltao.",
"ui.space_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantéño e sóltao.",
"ui.space_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantéño e sóltao.",
"ui.space_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantéño e sóltao.",
"ui.spacing": "Espazado",
"ui.spacing_2": "Espazado 2",
"ui.sphere": "Esfera",
@@ -1836,6 +1975,7 @@
"ui.workspace_design_tabs": "Modelo · Superficie · Montaxe · Inserir · Inspeccionar · Xestionar · Vista",
"ui.workspace_drawing_description": "Planos 2D con vistas, cotas e cartelas",
"ui.workspace_footer": "Todos os espazos de traballo usan o mesmo deseño, historial e selección; só cambian as pestanas da cinta. O selector bloquéase mentres se edita un bosquexo ou unha operación.",
"ui.workspace_layout": "Disposición do espazo de traballo",
"ui.workspace_manufacture_description": "Laminado, traxectorias de ferramenta e configuración",
"ui.workspace_planned": "Previsto",
"ui.workspace_render_description": "Vista previa do produto, axustes de escena e imaxes renderizadas",
+117 -35
View File
@@ -72,6 +72,7 @@
"Choose…": "Válasszon…",
"Circular": "Kör alakú",
"Circumference": "Kerület",
"Clockwise": "Óramutató járásával megegyező",
"Close draft": "Vázlat bezárása",
"Collisions": "Ütközések",
"Colour": "Szín",
@@ -167,6 +168,7 @@
"HISTORY": "ELŐZMÉNYEK",
"Height": "Magasság",
"Hidden edges": "Rejtett élek",
"History layout": "Előzmények elrendezése",
"Holes": "Furatok",
"Holes (edges on them)": "Furatok (rajtuk lévő élek)",
"Home view": "Kezdőnézet",
@@ -232,7 +234,6 @@
"No crash reports.": "Nincsenek összeomlási jelentések.",
"No matching appearance presets.": "Nincs egyező megjelenés-előbeállítás.",
"No matching appearances": "Nincs egyező megjelenés",
"No visible bodies to export.": "Nincs exportálható látható test.",
"None": "Nincs",
"Not available yet": "Még nem érhető el",
"Notch": "Bevágás",
@@ -321,6 +322,7 @@
"Search commands": "Parancsok keresése",
"Search this library": "Keresés ebben a könyvtárban",
"Section": "Metszet",
"Section position": "Szelvény helyzete",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Más kijelölése: tartsa nyomva a bal gombot egy helyen a modellen (vagy jobb kattintás › Más kijelölése…) a kurzor alatti elemek listázásához; kattintson újra ugyanarra a helyre a következő elem kijelöléséhez. A középső gomb dupla kattintása a kezdőnézetre vált (Beállítások › Navigáció).",
"Select bodies (a face picks its body)": "Testek kijelölése (egy lap a testét jelöli ki)",
"Select bodies or faces instead": "Inkább testek vagy lapok kijelölése",
@@ -332,6 +334,7 @@
"Show Agent Cursor": "Ügynök kurzorának megjelenítése",
"Show in folder": "Megjelenítés a mappában",
"Show the pointer an AI agent moves (#35)": "Az AI-ügynök által mozgatott mutató megjelenítése (#35)",
"Show tool at cursor": "Eszköz megjelenítése a kurzornál",
"Signed axes": "Előjeles tengelyek",
"Size": "Méret",
"Sketch constraints": "Vázlatkényszerek",
@@ -344,9 +347,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "A SolidCraft bezárult, mielőtt ezek a tervek mentve lettek volna. Az utolsó automatikus mentésük megnyitható.",
"Specialise": "Specializálás",
"Speed": "Sebesség",
"Square": "Négyzet",
"Stand it up": "Felállítás",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Felállítás: negyedfordulat az X körül, egyetlen visszavonható Mozgatás elemként.",
"Stand this part up?": "Felállítja ezt az alkatrészt?",
"Start angle": "Kezdőszög",
"Start bulge": "Kezdő ívelés",
"Start continuity": "Kezdő folytonosság",
"Step": "Lépés",
@@ -411,6 +416,7 @@
"angle, height": "szög, magasság",
"appearance.dielectric_ior": "Dielektromos törésmutató",
"appearance.metallic": "Fémes",
"appearance.only_available": "Csak elérhető megjelenések",
"appearance.pbr_help": "Tekintse meg ezt a felületet Renderelésben vagy termék-PNG-ben. Nincs fénytörés vagy áteresztés.",
"appearance.pbr_opaque": "Átlátszatlan PBR",
"appearance.roughness": "Érdesség",
@@ -529,6 +535,10 @@
"cmd.file_insert_step": "STEP beszúrása",
"cmd.file_save_mesh": "Mentés hálóként",
"cmd.file_slicers": "Telepített szeletelők",
"cmd.frame_member": "Szerkezeti elem",
"cmd.frame_profile_insert": "Profil beszúrása",
"cmd.frame_profile_list": "Profilkönyvtár",
"cmd.frame_profile_properties": "Profil tulajdonságai",
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
"cmd.inspect_center_of_mass": "Tömegközéppont",
"cmd.inspect_clearance": "Hézag",
@@ -568,6 +578,7 @@
"cmd.material_create": "Új fizikai anyag",
"cmd.material_delete": "Fizikai anyag törlése",
"cmd.material_edit": "Fizikai anyag szerkesztése",
"cmd.material_inspect": "Fizikai anyag vizsgálata",
"cmd.material_list": "Anyagok listázása",
"cmd.material_properties_directional": "Irányfüggő rugalmassági modulus",
"cmd.material_properties_validate": "Fizikai anyagadatok ellenőrzése",
@@ -856,19 +867,57 @@
"help.mit": "MIT licenc",
"help.notice": "NOTICE",
"help.third_party": "Harmadik felek licencei",
"home.back": "Vissza ide: {name}",
"home.crashes": "Összeomlási jelentések",
"home.design_workspace": "Tervezés",
"home.documentation": "Dokumentáció",
"home.export": "Exportálás",
"home.export_hint": "Válasszon formátumot az aktuális tervhez.",
"home.export_json_hint": "Szerkeszthető terv és parancselőzmények",
"home.export_mesh_hint": "Háló nyomtatáshoz vagy megosztáshoz",
"home.export_step_hint": "Pontos testgeometria",
"home.import": "Importálás…",
"home.import_hint": "STEP, IGES vagy háló importálása új tervbe",
"home.in_this_browser": "ebben a böngészőben",
"home.missing": "hiányzik",
"home.month_01": "jan.",
"home.month_02": "febr.",
"home.month_03": "márc.",
"home.month_04": "ápr.",
"home.month_05": "máj.",
"home.month_06": "jún.",
"home.month_07": "júl.",
"home.month_08": "aug.",
"home.month_09": "szept.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Új terv hüvelykben",
"home.new_inches_hint": "Hüvelyk · a Tervezés munkaterületen nyílik meg",
"home.new_mm": "Új terv",
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
"home.open": "Megnyitás…",
"home.opened_days_ago": "Megnyitva {count} napja",
"home.opened_hours_ago": "Megnyitva {count} órája",
"home.opened_just_now": "Épp most megnyitva",
"home.opened_minutes_ago": "Megnyitva {count} perce",
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
"home.open_now": "Jelenleg megnyitva",
"home.opened_days_ago": "{count} napja",
"home.opened_hours_ago": "{count} órája",
"home.opened_just_now": "Épp most",
"home.opened_minutes_ago": "{count} perce",
"home.opening": "megnyitás…",
"home.properties": "Terv tulajdonságai",
"home.recent": "Legutóbbi",
"home.recent_empty": "A megnyitott vagy mentett tervek itt jelennek meg.",
"home.recovery_count": "Helyreállítható tervek: {count}",
"home.recovery_empty": "Nincs helyreállítható terv.",
"home.recovery_hint": "A SolidCraft a tervek mentése előtt bezárult.",
"home.report_issue": "Hiba jelentése",
"home.samples": "Minták",
"home.settings": "Beállítások",
"home.short_date": "{month} {day}.",
"home.shortcuts": "Billentyűparancsok",
"home.start_hint": "Hozzon létre egy tervet, nyisson meg egy legutóbbi fájlt, vagy fedezzen fel egy mintát.",
"home.whats_new": "Újdonságok",
"home.yesterday": "Tegnap",
"install.button_tip": "A SolidCraft telepítése alkalmazásként: saját ablakban nyílik meg, és offline is működik",
"install.dock_1": "Válassza a Fájl › Hozzáadás a Dockhoz lehetőséget.",
"install.dock_2": "Kattintson a Hozzáadás gombra.",
@@ -1021,7 +1070,7 @@
"search.try": "Írja be egy parancs nevét, a terv egy elemét vagy egy paramétert. Parancs értékekkel való futtatásához írja be az azonosítóját és egy JSON-objektumot: {example}",
"search.valid": "Érvényes",
"settings.display_quality_note": "A finomabb szintek simábban rajzolják az ívelt lapokat és éleket, de tovább tart a hálózás. Csak a nézetablak változik, a terv és az exportok soha.",
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában. Bekapcsolt állapotban a nézet folyamatosan újrarajzolódik.",
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában.",
"settings.measure_axis_distances": "X/Y/Z távolságok megjelenítése",
"settings.measure_axis_distances_help": "A világtengely-összetevők méreteit is megjeleníti a teljes távolság mellett, amíg a Mérés nyitva van.",
"settings.msaa_applies_on_restart": "Az élsimítás a SolidCraft következő indításakor lép érvénybe",
@@ -1082,7 +1131,50 @@
"trace.stopped_by_contact": "Vége a(z) {sample}. mintánál: {a} áthatolna ezen: {b}",
"trace.stopped_by_loop": "Vége a(z) {sample}. mintánál: a(z) {joint} csuklón átmenő hurok nem zárható tovább",
"ui.1_degree_of_freedom": "1 szabadságfok",
"ui.3d_mouse_calibrate": "Kalibrálás…",
"ui.3d_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
"ui.3d_mouse_calibrate_title": "3D-egér kalibrálása",
"ui.3d_mouse_calibrate_wait": "Mozdítsa meg egyszer a 3D-egeret, hogy a SolidCraft lássa.",
"ui.3d_mouse_calibrated": "Kalibrálva",
"ui.3d_mouse_calibration": "3D-egér kalibrálása",
"ui.3d_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
"ui.3d_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
"ui.3d_mouse_circles_help": "A körök azt mutatják, mit küld az eszköz kalibrálás után. A tesztmodell azt mutatja, mit tesz ezzel a nézet a megfordítás, sebesség és tengelykiosztás után.",
"ui.3d_mouse_dead_zone": "Holtsáv",
"ui.3d_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
"ui.3d_mouse_denied": "A SolidCraft látja a 3D-egeret, de nem olvashatja. Az udev-szabályt a 3D-egér beállítási jegyzetei tartalmazzák.",
"ui.3d_mouse_denied_short": "Megvan, de nem olvasható",
"ui.3d_mouse_driver_fallback": "Egyelőre a spacenavd-n át olvassa: magát az eszközt nem sikerült megnyitni.",
"ui.3d_mouse_forget": "Kalibrálás elfelejtése",
"ui.3d_mouse_ghost_left_viewport": "Bal nézet",
"ui.3d_mouse_ghost_left_window": "Bal ablak",
"ui.3d_mouse_ghost_position": "Helyzet",
"ui.3d_mouse_ghost_right_viewport": "Jobb nézet",
"ui.3d_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
"ui.3d_mouse_navigate": "Navigálás a 3D-egérrel",
"ui.3d_mouse_none": "Nem található 3D-egér",
"ui.3d_mouse_not_calibrated": "Nincs kalibrálva",
"ui.3d_mouse_offer": "A 3D-egér kalibrálása kijavítja a felcserélt vagy megfordított tengelyeket, és beállítja a sebességét és az érzetét.",
"ui.3d_mouse_offer_never": "Ne jelenjen meg többé",
"ui.3d_mouse_offer_swap": "Váltás Hidrawra (közvetlen illesztőprogram)",
"ui.3d_mouse_reading": "Beolvasás",
"ui.3d_mouse_reset_view": "Nézet alaphelyzetbe állítása",
"ui.3d_mouse_restore": "3D-egér alapértékeinek visszaállítása",
"ui.3d_mouse_restore_ask": "Visszaállítja a 3D-egér beállításait az alapértékekre? A kalibrálás és a gombkiosztás megmarad.",
"ui.3d_mouse_speed": "3D-egér sebessége",
"ui.3d_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
"ui.3d_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
"ui.3d_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
"ui.3d_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
"ui.3d_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
"ui.3d_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
"ui.3d_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
"ui.3d_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
"ui.3d_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
"ui.3d_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
"ui.3d_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
"ui.3d_mouse_waiting": "Mozgásra vár",
"ui.3d_mouse_wiggle": "Ha van csatlakoztatva egy, mozgassa meg egy kicsit.",
"ui.3d_sketch": "3D vázlat",
"ui.activate": "Aktiválás",
"ui.active_component": "Aktív komponens",
@@ -1092,6 +1184,7 @@
"ui.agent_chat_add": "Fájlok csatolása",
"ui.agent_chat_add_file": "Válasszon fájlt…",
"ui.agent_chat_allow": "Engedélyezés",
"ui.agent_chat_allow_once": "Egyszer engedélyezi",
"ui.agent_chat_allowed": "Engedélyezve",
"ui.agent_chat_answered": "Válasz: {option}",
"ui.agent_chat_approval": "Engedélyezi az ügynöknek ezt?",
@@ -1143,12 +1236,16 @@
"ui.agent_chat_files_left_out": "Egy üzenethez legfeljebb {max} fájl tartozhat; kihagyva: {files}",
"ui.agent_chat_files_loading": "Várakozás a fájlok beolvasására",
"ui.agent_chat_files_max": "Egy üzenetben legfeljebb {max} fájl lehet",
"ui.agent_chat_flatpak_none_found": "Nem található kódolóügynök. A Flatpak homokozó alapértelmezés szerint nem látja a saját mappájába telepített ügynököket, és nincs hálózati hozzáférése. Az nvm-mel vagy a ~/.local/bin mappába telepített Claude Code és ACP-adaptere engedélyezéséhez futtassa ezt a parancsot egy terminálban, majd indítsa újra a SolidCraftot. Hozzáférést ad a felsorolt programokhoz és bejelentkezési fájlokhoz, valamint a hálózathoz:",
"ui.agent_chat_import_failed": "{name}: az importálás sikertelen: {error}",
"ui.agent_chat_intro": "Csevegjen egy már használt kódolóügynökkel vagy egy választott modellvégponttal. A megnyitott terven a SolidCraft eszközein keresztül dolgozik: a módosításai láthatók közben, és üzenetenként egy lépésben vonhatók vissza. Hogy mit tehet kérdés nélkül, azt a Beállítások > AI menüpontban adhatja meg.",
"ui.agent_chat_key_in_env": "kulcs beállítva",
"ui.agent_chat_left": "Bal",
"ui.agent_chat_limit": "Az ügynök elérte az egy üzenetre eső {limit} módosítás korlátját. Küldjön újabb üzenetet a folytatáshoz.",
"ui.agent_chat_minimize": "Összecsukás sávvá",
"ui.agent_chat_mode_all": "Minden",
"ui.agent_chat_mode_ask": "Kérdez",
"ui.agent_chat_mode_edits": "Szerkesztés",
"ui.agent_chat_model_tip": "Az ügynök által használt modell. A választás megmarad ennél az ügynöknél.",
"ui.agent_chat_new": "Új csevegés",
"ui.agent_chat_new_confirm": "Új csevegést indít? A jelenlegi törlődik.",
@@ -1161,6 +1258,7 @@
"ui.agent_chat_none_found_short": "Nincs ügynök",
"ui.agent_chat_not_installed": "Nincs telepítve",
"ui.agent_chat_not_signed_in": "nincs bejelentkezve",
"ui.agent_chat_open_settings": "Beállítások megnyitása",
"ui.agent_chat_own_window": "Saját ablak",
"ui.agent_chat_paste_picture": "Kép beillesztése",
"ui.agent_chat_permission": "Az ügynök futtatást kér: {title}",
@@ -1185,6 +1283,8 @@
"ui.agent_chat_send": "Küldés",
"ui.agent_chat_send_now": "Küldés most",
"ui.agent_chat_send_now_tip": "Az ügynök jelenlegi lépésének leállítása és ennek küldése következőként (Ctrl+Enter)",
"ui.agent_chat_setup_body": "A SolidCraft a számítógépre telepített programozóügynökkel kommunikál. Válasszon egyet itt: Beállítások › AI.",
"ui.agent_chat_setup_title": "Asszisztens beállítása",
"ui.agent_chat_side": "Ügynök-csevegés oldala",
"ui.agent_chat_signed_in": "bejelentkezve",
"ui.agent_chat_state_auth": "Bejelentkezés szükséges",
@@ -1201,6 +1301,7 @@
"ui.agent_chat_undo_label": "Ügynök-csevegés: {message}",
"ui.agent_chat_undone": "(visszavonva)",
"ui.agent_chat_view_attached": "Nézet csatolva",
"ui.agent_chat_waiting": "Önre vár",
"ui.agent_cursor": "Ügynök kurzora",
"ui.agent_cursor_speed": "Ügynök kurzorának sebessége",
"ui.ai_add": "Hozzáadás",
@@ -1258,7 +1359,6 @@
"ui.ai_permissions": "Jogosultságok",
"ui.ai_preset": "Előbeállítás",
"ui.ai_remove": "Eltávolítás",
"ui.ai_request_limit": "egy üzenetre eső 24 kérés után leállt; küldjön újabb üzenetet a folytatáshoz",
"ui.ai_reset": "Visszaállítás",
"ui.ai_settings": "AI-beállítások…",
"ui.ai_show_features": "AI-funkciók megjelenítése",
@@ -1455,6 +1555,7 @@
"ui.grid_follows_view_hint": "Az oldalnézetek a talaj helyett a feléjük néző függőleges síkon mutatják a rácsot",
"ui.ground_shadow": "Talajárnyék",
"ui.groupSelected": "Kijelöltek csoportosítása",
"ui.hatch": "Sraffozás",
"ui.heading_add_ins": "BŐVÍTMÉNYEK",
"ui.heading_assemble": "ÖSSZESZERELÉS",
"ui.heading_box": "DOBOZ",
@@ -1528,6 +1629,11 @@
"ui.hideOrigin": "Origó elrejtése",
"ui.hidePlanes": "Síkok elrejtése",
"ui.hideProfile": "Profil elrejtése",
"ui.history_back": "Egy lépéssel vissza",
"ui.history_below_browser": "Böngésző alatt",
"ui.history_first": "Ugrás az elejére",
"ui.history_forward": "Egy lépéssel előre",
"ui.history_last": "Ugrás a végére",
"ui.history_title": "Előzmények",
"ui.hole": "Furat",
"ui.hole_tap_type": "Furat menettípusa",
@@ -1545,6 +1651,8 @@
"ui.joints": "Csuklók",
"ui.keyboard_shortcuts": "Billentyűparancsok",
"ui.language": "Nyelv",
"ui.layout_compact": "Kompakt",
"ui.layout_floating": "Lebegő",
"ui.length_decimals": "Hossz tizedesjegyei",
"ui.light": "Világos",
"ui.line": "Vonal",
@@ -1682,6 +1790,7 @@
"ui.reverse_zoom_direction": "Nagyítási irány megfordítása",
"ui.revolve": "Forgatás",
"ui.ribbon_exit": "Kilépés",
"ui.ribbon_labels": "Szöveg megjelenítése a menüszalag ikonjai alatt",
"ui.ribbon_pin_footer": "A rögzített parancsok megjelennek a menüszalagon. Egy sor fölé víve rögzíthető vagy feloldható.",
"ui.ribbon_workspace": "Munkaterület",
"ui.right": "Jobb",
@@ -1737,34 +1846,6 @@
"ui.smooth_wheel_zoom": "Sima görgőnagyítás",
"ui.smooth_wheel_zoom_tip": "Kapcsolja ki az egyes görgőesemények azonnali alkalmazásához, kifutás nélkül",
"ui.snap": "Illesztés",
"ui.space_mouse_calibrate": "Kalibrálás…",
"ui.space_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
"ui.space_mouse_calibrate_title": "3D-egér kalibrálása",
"ui.space_mouse_calibrated": "Kalibrálva",
"ui.space_mouse_calibration": "3D-egér kalibrálása",
"ui.space_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
"ui.space_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
"ui.space_mouse_dead_zone": "Holtsáv",
"ui.space_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
"ui.space_mouse_forget": "Kalibrálás elfelejtése",
"ui.space_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
"ui.space_mouse_navigate": "Navigálás SpaceNavigator / SpaceMouse eszközzel",
"ui.space_mouse_none": "Nem található 3D-egér",
"ui.space_mouse_not_calibrated": "Nincs kalibrálva",
"ui.space_mouse_reading": "Beolvasás",
"ui.space_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
"ui.space_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
"ui.space_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
"ui.space_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
"ui.space_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
"ui.space_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
"ui.space_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
"ui.space_mouse_step_tilt_back": "Billentse hátra a kupakot (az elülső él felfelé), tartsa, majd engedje el.",
"ui.space_mouse_step_tilt_forward": "Billentse előre a kupakot (az elülső él lefelé), tartsa, majd engedje el.",
"ui.space_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
"ui.space_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
"ui.space_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
"ui.space_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
"ui.spacing": "Térköz",
"ui.spacing_2": "2. térköz",
"ui.sphere": "Gömb",
@@ -1836,6 +1917,7 @@
"ui.workspace_design_tabs": "Modell · Felület · Összeszerelés · Beszúrás · Vizsgálat · Kezelés · Nézet",
"ui.workspace_drawing_description": "2D rajzok nézetekkel, méretekkel és címblokkokkal",
"ui.workspace_footer": "Minden munkaterület ugyanazon a terven, előzményeken és kijelölésen dolgozik; csak a menüszalag lapjai változnak. A váltó zárolva van, amíg vázlatot vagy elemet szerkeszt.",
"ui.workspace_layout": "Munkaterület elrendezése",
"ui.workspace_manufacture_description": "Szeletelés, szerszámutak és beállítás",
"ui.workspace_planned": "Tervezett",
"ui.workspace_render_description": "Terméknézet, jelenetbeállítások és renderelt képek",
+117 -35
View File
@@ -72,6 +72,7 @@
"Choose…": "Pilih…",
"Circular": "Melingkar",
"Circumference": "Keliling",
"Clockwise": "Searah jarum jam",
"Close draft": "Tutup draf",
"Collisions": "Tabrakan",
"Colour": "Warna",
@@ -167,6 +168,7 @@
"HISTORY": "RIWAYAT",
"Height": "Tinggi",
"Hidden edges": "Tepi tersembunyi",
"History layout": "Tata letak riwayat",
"Holes": "Lubang",
"Holes (edges on them)": "Lubang (tepi di atasnya)",
"Home view": "Tampilan awal",
@@ -232,7 +234,6 @@
"No crash reports.": "Tidak ada laporan crash.",
"No matching appearance presets.": "Tidak ada preset tampilan yang cocok.",
"No matching appearances": "Tidak ada tampilan yang cocok",
"No visible bodies to export.": "Tidak ada bodi terlihat untuk diekspor.",
"None": "Tidak ada",
"Not available yet": "Belum tersedia",
"Notch": "Takik",
@@ -321,6 +322,7 @@
"Search commands": "Cari perintah",
"Search this library": "Cari di pustaka ini",
"Section": "Penampang",
"Section position": "Posisi penampang",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: tahan tombol kiri diam di model (atau klik kanan › Select Other…) untuk mendaftar semua yang ada di bawah kursor; klik lagi di titik yang sama untuk memilih benda berikutnya. Klik dua kali tombol tengah untuk ke Home (Preferensi › Navigasi).",
"Select bodies (a face picks its body)": "Pilih bodi (memilih muka berarti memilih bodinya)",
"Select bodies or faces instead": "Pilih bodi atau muka saja",
@@ -332,6 +334,7 @@
"Show Agent Cursor": "Tampilkan Kursor Agen",
"Show in folder": "Tampilkan di folder",
"Show the pointer an AI agent moves (#35)": "Tampilkan penunjuk yang digerakkan agen AI (#35)",
"Show tool at cursor": "Tampilkan alat di dekat kursor",
"Signed axes": "Sumbu bertanda",
"Size": "Ukuran",
"Sketch constraints": "Batasan sketsa",
@@ -344,9 +347,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft tertutup sebelum desain ini disimpan. Simpan otomatis terakhirnya dapat dibuka.",
"Specialise": "Spesialisasikan",
"Speed": "Kecepatan",
"Square": "Persegi",
"Stand it up": "Tegakkan",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: putaran seperempat terhadap X, sebagai satu fitur Move yang dapat dibatalkan.",
"Stand this part up?": "Tegakkan part ini?",
"Start angle": "Sudut awal",
"Start bulge": "Bulge awal",
"Start continuity": "Kontinuitas awal",
"Step": "Langkah",
@@ -411,6 +416,7 @@
"angle, height": "sudut, tinggi",
"appearance.dielectric_ior": "IOR Dielektrik",
"appearance.metallic": "Metalik",
"appearance.only_available": "Hanya tampilan yang tersedia",
"appearance.pbr_help": "Lihat permukaan ini di Render atau PNG produk. Tanpa refraksi atau transmisi.",
"appearance.pbr_opaque": "PBR Opak",
"appearance.roughness": "Kekasaran",
@@ -529,6 +535,10 @@
"cmd.file_insert_step": "Sisipkan STEP",
"cmd.file_save_mesh": "Simpan Sebagai Mesh",
"cmd.file_slicers": "Slicer Terpasang",
"cmd.frame_member": "Batang Rangka",
"cmd.frame_profile_insert": "Sisipkan Profil",
"cmd.frame_profile_list": "Pustaka Profil",
"cmd.frame_profile_properties": "Properti Profil",
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
"cmd.inspect_center_of_mass": "Pusat Massa",
"cmd.inspect_clearance": "Kelonggaran",
@@ -568,6 +578,7 @@
"cmd.material_create": "Material Fisik Baru",
"cmd.material_delete": "Hapus Material Fisik",
"cmd.material_edit": "Edit Material Fisik",
"cmd.material_inspect": "Periksa Material Fisik",
"cmd.material_list": "Daftar Material",
"cmd.material_properties_directional": "Modulus Elastis Arah",
"cmd.material_properties_validate": "Validasi Data Material Fisik",
@@ -856,19 +867,57 @@
"help.mit": "Lisensi MIT",
"help.notice": "NOTICE",
"help.third_party": "Lisensi pihak ketiga",
"home.back": "Kembali ke {name}",
"home.crashes": "Laporan kerusakan",
"home.design_workspace": "Desain",
"home.documentation": "Dokumentasi",
"home.export": "Ekspor",
"home.export_hint": "Pilih format untuk desain saat ini.",
"home.export_json_hint": "Desain yang dapat diedit dan riwayat perintah",
"home.export_mesh_hint": "Mesh untuk pencetakan atau berbagi",
"home.export_step_hint": "Geometri benda padat yang presisi",
"home.import": "Impor…",
"home.import_hint": "STEP, IGES, atau mesh ke dalam desain baru",
"home.in_this_browser": "di peramban ini",
"home.missing": "hilang",
"home.month_01": "Jan",
"home.month_02": "Feb",
"home.month_03": "Mar",
"home.month_04": "Apr",
"home.month_05": "Mei",
"home.month_06": "Jun",
"home.month_07": "Jul",
"home.month_08": "Agu",
"home.month_09": "Sep",
"home.month_10": "Okt",
"home.month_11": "Nov",
"home.month_12": "Des",
"home.new_inches": "Desain baru dalam inci",
"home.new_inches_hint": "Inci · terbuka di Desain",
"home.new_mm": "Desain baru",
"home.new_mm_hint": "Milimeter · terbuka di Desain",
"home.open": "Buka…",
"home.opened_days_ago": "Dibuka {count} hari lalu",
"home.opened_hours_ago": "Dibuka {count} jam lalu",
"home.opened_just_now": "Baru saja dibuka",
"home.opened_minutes_ago": "Dibuka {count} mnt lalu",
"home.open_hint": "Desain .solidcraft ({shortcut})",
"home.open_now": "Sedang terbuka",
"home.opened_days_ago": "{count} hari lalu",
"home.opened_hours_ago": "{count} jam lalu",
"home.opened_just_now": "Baru saja",
"home.opened_minutes_ago": "{count} mnt lalu",
"home.opening": "membuka…",
"home.properties": "Properti desain",
"home.recent": "Terbaru",
"home.recent_empty": "Desain yang Anda buka atau simpan akan muncul di sini.",
"home.recovery_count": "Desain yang dapat dipulihkan: {count}",
"home.recovery_empty": "Tidak ada desain yang dapat dipulihkan.",
"home.recovery_hint": "SolidCraft ditutup sebelum desain ini disimpan.",
"home.report_issue": "Laporkan Masalah",
"home.samples": "Contoh",
"home.settings": "Pengaturan",
"home.short_date": "{day} {month}",
"home.shortcuts": "Pintasan papan ketik",
"home.start_hint": "Buat desain, buka berkas terbaru, atau jelajahi contoh.",
"home.whats_new": "Yang baru",
"home.yesterday": "Kemarin",
"install.button_tip": "Pasang SolidCraft sebagai aplikasi: terbuka di jendelanya sendiri dan berfungsi offline",
"install.dock_1": "Pilih File › Add to Dock.",
"install.dock_2": "Klik Add.",
@@ -1021,7 +1070,7 @@
"search.try": "Ketik nama perintah, fitur di desain ini, atau parameter. Untuk menjalankan perintah dengan nilai, ketik id-nya dan objek JSON: {example}",
"search.valid": "Valid",
"settings.display_quality_note": "Level lebih halus menggambar muka dan tepi lengkung lebih mulus dan butuh waktu meshing lebih lama. Hanya viewport yang berubah, tidak pernah desain atau ekspor.",
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan. Saat aktif, tampilan digambar ulang terus-menerus.",
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan.",
"settings.measure_axis_distances": "Tampilkan jarak X/Y/Z",
"settings.measure_axis_distances_help": "Tampilkan dimensi komponen sumbu dunia di samping jarak total saat Ukur terbuka.",
"settings.msaa_applies_on_restart": "Anti-aliasing berlaku saat SolidCraft dimulai berikutnya",
@@ -1082,7 +1131,50 @@
"trace.stopped_by_contact": "Berakhir di sampel {sample}: {a} akan menembus {b}",
"trace.stopped_by_loop": "Berakhir di sampel {sample}: loop melalui {joint} tidak dapat menutup lebih jauh",
"ui.1_degree_of_freedom": "1 derajat kebebasan",
"ui.3d_mouse_calibrate": "Kalibrasi…",
"ui.3d_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
"ui.3d_mouse_calibrate_title": "Kalibrasi mouse 3D",
"ui.3d_mouse_calibrate_wait": "Gerakkan mouse 3D sekali agar SolidCraft dapat melihatnya.",
"ui.3d_mouse_calibrated": "Terkalibrasi",
"ui.3d_mouse_calibration": "Kalibrasi mouse 3D",
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
"ui.3d_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
"ui.3d_mouse_circles_help": "Lingkaran menunjukkan apa yang dikirim perangkat setelah kalibrasi. Model uji menunjukkan apa yang dilakukan tampilan dengannya, setelah pembalikan, kecepatan, dan penetapan sumbu.",
"ui.3d_mouse_dead_zone": "Zona mati",
"ui.3d_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
"ui.3d_mouse_denied": "SolidCraft melihat mouse 3D tetapi tidak diizinkan membacanya. Lihat catatan penyiapan mouse 3D untuk aturan udev.",
"ui.3d_mouse_denied_short": "Ditemukan, tetapi tidak bisa dibaca",
"ui.3d_mouse_driver_fallback": "Sementara dibaca lewat spacenavd: perangkatnya sendiri tidak dapat dibuka.",
"ui.3d_mouse_forget": "Lupakan kalibrasi",
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
"ui.3d_mouse_ghost_left_window": "Jendela kiri",
"ui.3d_mouse_ghost_position": "Posisi",
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
"ui.3d_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
"ui.3d_mouse_navigate": "Navigasi dengan mouse 3D",
"ui.3d_mouse_none": "Mouse 3D tidak ditemukan",
"ui.3d_mouse_not_calibrated": "Belum dikalibrasi",
"ui.3d_mouse_offer": "Mengkalibrasi mouse 3D memperbaiki sumbu yang tertukar atau terbalik dan mengatur kecepatan serta rasanya.",
"ui.3d_mouse_offer_never": "Jangan tampilkan lagi",
"ui.3d_mouse_offer_swap": "Beralih ke Hidraw (driver langsung)",
"ui.3d_mouse_reading": "Pembacaan",
"ui.3d_mouse_reset_view": "Atur ulang tampilan",
"ui.3d_mouse_restore": "Pulihkan bawaan mouse 3D",
"ui.3d_mouse_restore_ask": "Kembalikan pengaturan mouse 3D ke bawaan? Kalibrasi dan pemetaan tombol tetap ada.",
"ui.3d_mouse_speed": "Kecepatan mouse 3D",
"ui.3d_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
"ui.3d_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
"ui.3d_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
"ui.3d_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
"ui.3d_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
"ui.3d_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
"ui.3d_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
"ui.3d_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
"ui.3d_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
"ui.3d_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
"ui.3d_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
"ui.3d_mouse_waiting": "Menunggu gerakan",
"ui.3d_mouse_wiggle": "Jika Anda memasang satu, goyangkan sedikit.",
"ui.3d_sketch": "Sketsa 3D",
"ui.activate": "Aktifkan",
"ui.active_component": "Komponen aktif",
@@ -1092,6 +1184,7 @@
"ui.agent_chat_add": "Lampirkan file",
"ui.agent_chat_add_file": "Pilih file…",
"ui.agent_chat_allow": "Izinkan",
"ui.agent_chat_allow_once": "Izinkan sekali",
"ui.agent_chat_allowed": "Diizinkan",
"ui.agent_chat_answered": "Jawaban: {option}",
"ui.agent_chat_approval": "Izinkan agen melakukan ini?",
@@ -1143,12 +1236,16 @@
"ui.agent_chat_files_left_out": "Maksimal {max} file per pesan; tidak disertakan: {files}",
"ui.agent_chat_files_loading": "Menunggu file selesai dibaca",
"ui.agent_chat_files_max": "Maksimal {max} file per pesan",
"ui.agent_chat_flatpak_none_found": "Agen coding tidak ditemukan. Secara bawaan, sandbox Flatpak tidak dapat melihat agen yang terpasang di folder rumah Anda dan tidak memiliki akses jaringan. Untuk mengizinkan Claude Code dan adaptor ACP-nya yang dipasang melalui nvm atau di ~/.local/bin, jalankan perintah ini di terminal, lalu mulai ulang SolidCraft. Perintah ini memberi akses ke program dan berkas masuk yang disebutkan, serta akses jaringan:",
"ui.agent_chat_import_failed": "{name}: impor gagal: {error}",
"ui.agent_chat_intro": "Mengobrol dengan agen coding yang sudah Anda pakai, atau endpoint model pilihan Anda. Agen bekerja pada desain yang terbuka lewat alat SolidCraft: perubahannya tampil saat dibuat dan dapat diurungkan satu langkah per pesan. Yang boleh dilakukannya tanpa bertanya diatur di Preferensi > AI.",
"ui.agent_chat_key_in_env": "key diatur",
"ui.agent_chat_left": "Kiri",
"ui.agent_chat_limit": "Agen mencapai batas {limit} perubahan untuk satu pesan. Kirim pesan lain agar agen melanjutkan.",
"ui.agent_chat_minimize": "Kecilkan menjadi strip",
"ui.agent_chat_mode_all": "Semua",
"ui.agent_chat_mode_ask": "Tanya",
"ui.agent_chat_mode_edits": "Edit",
"ui.agent_chat_model_tip": "Model yang dipakai agen. Pilihan Anda diingat untuk agen ini.",
"ui.agent_chat_new": "Chat baru",
"ui.agent_chat_new_confirm": "Mulai chat baru? Chat saat ini akan dibersihkan.",
@@ -1161,6 +1258,7 @@
"ui.agent_chat_none_found_short": "Agen tidak ditemukan",
"ui.agent_chat_not_installed": "Belum terpasang",
"ui.agent_chat_not_signed_in": "belum masuk",
"ui.agent_chat_open_settings": "Buka preferensi",
"ui.agent_chat_own_window": "Jendela sendiri",
"ui.agent_chat_paste_picture": "Tempel gambar",
"ui.agent_chat_permission": "Agen meminta menjalankan: {title}",
@@ -1185,6 +1283,8 @@
"ui.agent_chat_send": "Kirim",
"ui.agent_chat_send_now": "Kirim sekarang",
"ui.agent_chat_send_now_tip": "Hentikan langkah agen saat ini dan kirim ini berikutnya (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft berkomunikasi dengan agen pemrograman yang terpasang di komputer ini. Pilih agen di Preferensi › AI.",
"ui.agent_chat_setup_title": "Siapkan asisten",
"ui.agent_chat_side": "Sisi chat agen",
"ui.agent_chat_signed_in": "sudah masuk",
"ui.agent_chat_state_auth": "Perlu masuk",
@@ -1201,6 +1301,7 @@
"ui.agent_chat_undo_label": "Chat agen: {message}",
"ui.agent_chat_undone": "(diurungkan)",
"ui.agent_chat_view_attached": "Tampilan dilampirkan",
"ui.agent_chat_waiting": "Menunggu Anda",
"ui.agent_cursor": "Kursor agen",
"ui.agent_cursor_speed": "Kecepatan kursor agen",
"ui.ai_add": "Tambah",
@@ -1258,7 +1359,6 @@
"ui.ai_permissions": "Izin",
"ui.ai_preset": "Preset",
"ui.ai_remove": "Hapus",
"ui.ai_request_limit": "dihentikan setelah 24 permintaan untuk satu pesan; kirim pesan lain untuk melanjutkan",
"ui.ai_reset": "Reset",
"ui.ai_settings": "Pengaturan AI…",
"ui.ai_show_features": "Tampilkan fitur AI",
@@ -1455,6 +1555,7 @@
"ui.grid_follows_view_hint": "Tampilan samping menampilkan grid pada bidang tegak yang menghadapnya, bukan pada dasar",
"ui.ground_shadow": "Bayangan dasar",
"ui.groupSelected": "Kelompokkan yang Dipilih",
"ui.hatch": "Arsir",
"ui.heading_add_ins": "ADD-IN",
"ui.heading_assemble": "RAKIT",
"ui.heading_box": "KOTAK",
@@ -1528,6 +1629,11 @@
"ui.hideOrigin": "Sembunyikan Origin",
"ui.hidePlanes": "Sembunyikan Bidang",
"ui.hideProfile": "Sembunyikan Profil",
"ui.history_back": "Mundur satu langkah",
"ui.history_below_browser": "Di bawah browser",
"ui.history_first": "Ke awal",
"ui.history_forward": "Maju satu langkah",
"ui.history_last": "Ke akhir",
"ui.history_title": "Riwayat",
"ui.hole": "Lubang",
"ui.hole_tap_type": "Tipe Tap Lubang",
@@ -1545,6 +1651,8 @@
"ui.joints": "Sambungan",
"ui.keyboard_shortcuts": "Pintasan Keyboard",
"ui.language": "Bahasa",
"ui.layout_compact": "Ringkas",
"ui.layout_floating": "Mengambang",
"ui.length_decimals": "Desimal panjang",
"ui.light": "Terang",
"ui.line": "Garis",
@@ -1682,6 +1790,7 @@
"ui.reverse_zoom_direction": "Balik arah zoom",
"ui.revolve": "Revolve",
"ui.ribbon_exit": "Keluar",
"ui.ribbon_labels": "Tampilkan teks di bawah ikon pita",
"ui.ribbon_pin_footer": "Perintah yang disematkan tampil di ribbon. Arahkan kursor ke baris untuk menyematkan atau melepas sematan.",
"ui.ribbon_workspace": "Workspace",
"ui.right": "Kanan",
@@ -1737,34 +1846,6 @@
"ui.smooth_wheel_zoom": "Zoom roda halus",
"ui.smooth_wheel_zoom_tip": "Matikan untuk menerapkan setiap peristiwa roda segera, tanpa ekor penghalusan",
"ui.snap": "Snap",
"ui.space_mouse_calibrate": "Kalibrasi…",
"ui.space_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
"ui.space_mouse_calibrate_title": "Kalibrasi mouse 3D",
"ui.space_mouse_calibrated": "Terkalibrasi",
"ui.space_mouse_calibration": "Kalibrasi mouse 3D",
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
"ui.space_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
"ui.space_mouse_dead_zone": "Zona mati",
"ui.space_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
"ui.space_mouse_forget": "Lupakan kalibrasi",
"ui.space_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Mouse 3D tidak ditemukan",
"ui.space_mouse_not_calibrated": "Belum dikalibrasi",
"ui.space_mouse_reading": "Pembacaan",
"ui.space_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
"ui.space_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
"ui.space_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
"ui.space_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
"ui.space_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
"ui.space_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
"ui.space_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
"ui.space_mouse_step_tilt_back": "Miringkan cap ke belakang (tepi depan naik), tahan, lalu lepaskan.",
"ui.space_mouse_step_tilt_forward": "Miringkan cap ke depan (tepi depan turun), tahan, lalu lepaskan.",
"ui.space_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
"ui.space_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
"ui.space_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
"ui.space_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
"ui.spacing": "Jarak antar",
"ui.spacing_2": "Jarak antar 2",
"ui.sphere": "Bola",
@@ -1836,6 +1917,7 @@
"ui.workspace_design_tabs": "Model · Permukaan · Rakit · Sisipkan · Inspeksi · Kelola · Tampilan",
"ui.workspace_drawing_description": "Gambar kerja 2D dengan tampilan, dimensi, dan title block",
"ui.workspace_footer": "Setiap workspace bekerja pada desain, riwayat, dan pilihan yang sama; hanya tab ribbon yang berubah. Pengalih terkunci saat sketsa atau fitur sedang diedit.",
"ui.workspace_layout": "Tata letak ruang kerja",
"ui.workspace_manufacture_description": "Slicing, toolpath, dan setup",
"ui.workspace_planned": "Direncanakan",
"ui.workspace_render_description": "Pratinjau produk, pengaturan scene, dan gambar render",
+193 -36
View File
@@ -72,6 +72,7 @@
"Choose…": "Scegli…",
"Circular": "Circolare",
"Circumference": "Circonferenza",
"Clockwise": "Orario",
"Close draft": "Chiudi bozza",
"Collisions": "Collisioni",
"Colour": "Colore",
@@ -102,6 +103,7 @@
"Curv. Comb": "Pettine curv.",
"Curvature (G2)": "Curvatura (G2)",
"Curvature change (G3)": "Variazione di curvatura (G3)",
"Custom": "Personalizzato",
"Customize Toolbar": "Personalizza barra degli strumenti",
"Delete": "Elimina",
"Delete all": "Elimina tutto",
@@ -162,11 +164,13 @@
"From Bodies": "Dai corpi",
"From edges": "Spigoli di partenza",
"Full Length": "Lunghezza totale",
"Full thread depth": "Profondità filetto completa",
"Gap": "Gioco",
"Grid": "Griglia",
"HISTORY": "CRONOLOGIA",
"Height": "Altezza",
"Hidden edges": "Spigoli nascosti",
"History layout": "Disposizione cronologia",
"Holes": "Fori",
"Holes (edges on them)": "Fori (spigoli dei fori)",
"Home view": "Vista iniziale",
@@ -232,7 +236,8 @@
"No crash reports.": "Nessun rapporto di arresto anomalo.",
"No matching appearance presets.": "Nessun predefinito di aspetto corrispondente.",
"No matching appearances": "Nessun aspetto corrispondente",
"No visible bodies to export.": "Nessun corpo visibile da esportare.",
"No visible bodies or sketches to export.": "Nessun corpo o schizzo visibile da esportare.",
"Nominal diameter": "Diametro nominale",
"None": "Nessuno",
"Not available yet": "Non ancora disponibile",
"Notch": "Intaglio",
@@ -256,6 +261,7 @@
"Pick the tool bodies": "Seleziona i corpi utensile",
"Picked": "Selezionato",
"Picked point": "Punto selezionato",
"Pitch": "Passo",
"Place at the origin instead": "Posiziona invece nell'origine",
"Plastic Rules": "Regole per plastica",
"Playback": "Riproduzione",
@@ -321,6 +327,7 @@
"Search commands": "Cerca comandi",
"Search this library": "Cerca in questa libreria",
"Section": "Sezione",
"Section position": "Posizione della sezione",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleziona altro: tieni premuto il pulsante sinistro senza muovere il puntatore sul modello (o fai clic destro › Seleziona altro…) per elencare gli oggetti sotto il cursore; fai di nuovo clic nello stesso punto per selezionare l'oggetto successivo. Fai doppio clic con il pulsante centrale per tornare alla vista iniziale (Preferenze › Navigazione).",
"Select bodies (a face picks its body)": "Seleziona corpi (una faccia seleziona il corpo a cui appartiene)",
"Select bodies or faces instead": "Seleziona invece corpi o facce",
@@ -332,6 +339,7 @@
"Show Agent Cursor": "Mostra cursore dell'agente",
"Show in folder": "Mostra nella cartella",
"Show the pointer an AI agent moves (#35)": "Mostra il puntatore mosso da un agente IA (#35)",
"Show tool at cursor": "Mostra lo strumento accanto al cursore",
"Signed axes": "Assi con segno",
"Size": "Dimensioni",
"Sketch constraints": "Vincoli dello schizzo",
@@ -344,9 +352,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft si è chiuso prima del salvataggio di questi progetti. È possibile aprire il loro ultimo salvataggio automatico.",
"Specialise": "Specializza",
"Speed": "Velocità",
"Square": "Quadrato",
"Stand it up": "Raddrizza",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Raddrizza: un quarto di giro attorno a X, come singola lavorazione Sposta annullabile.",
"Stand this part up?": "Raddrizzare questa parte?",
"Start angle": "Angolo iniziale",
"Start bulge": "Rigonfiamento iniziale",
"Start continuity": "Continuità iniziale",
"Step": "Passo",
@@ -373,6 +383,7 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Questo elemento figlio è condiviso. Eliminandolo verrà rimosso da ogni copia del padre.",
"This child is shared. Position changes apply to every copy of its parent.": "Questo elemento figlio è condiviso. Le modifiche alla posizione si applicano a ogni copia del padre.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Questa faccia non consente un collegamento persistente. Scegli un'altra faccia o usa esplicitamente un piano fisso.",
"Thread depth": "Profondità filetto",
"To Offset": "Fino a una distanza",
"To edges": "Spigoli di arrivo",
"Tolerance": "Tolleranza",
@@ -411,6 +422,7 @@
"angle, height": "angolo, altezza",
"appearance.dielectric_ior": "Indice di rifrazione dielettrico",
"appearance.metallic": "Metallicità",
"appearance.only_available": "Solo aspetti disponibili",
"appearance.pbr_help": "Visualizza questa superficie in Rendering o in un PNG del prodotto. Senza rifrazione né trasmissione.",
"appearance.pbr_opaque": "PBR opaco",
"appearance.roughness": "Rugosità",
@@ -529,6 +541,10 @@
"cmd.file_insert_step": "Inserisci STEP",
"cmd.file_save_mesh": "Salva come mesh",
"cmd.file_slicers": "Slicer installati",
"cmd.frame_member": "Membro strutturale",
"cmd.frame_profile_insert": "Inserisci profilo",
"cmd.frame_profile_list": "Libreria profili",
"cmd.frame_profile_properties": "Proprietà del profilo",
"cmd.inspect_accessibility": "Analisi di accessibilità",
"cmd.inspect_center_of_mass": "Centro di massa",
"cmd.inspect_clearance": "Distanza libera",
@@ -568,6 +584,7 @@
"cmd.material_create": "Nuovo materiale fisico",
"cmd.material_delete": "Elimina materiale fisico",
"cmd.material_edit": "Modifica materiale fisico",
"cmd.material_inspect": "Ispeziona materiale fisico",
"cmd.material_list": "Elenca materiali",
"cmd.material_properties_directional": "Modulo elastico direzionale",
"cmd.material_properties_validate": "Convalida dati del materiale fisico",
@@ -831,7 +848,7 @@
"dxf.progress.projection": "Proiezione della geometria (3/5)",
"dxf.progress.write": "Scrittura DXF (5/5)",
"dxf.tolerance": "Tolleranza delle curve",
"dxf.tolerance_help": "Solo corpi visibili. Le curve usano segmenti rettilinei. Una tolleranza minore produce curve più precise e richiede più tempo e memoria.",
"dxf.tolerance_help": "Solo corpi visibili e schizzi mostrati. Le curve usano segmenti rettilinei. Una tolleranza minore produce curve più precise e richiede più tempo e memoria.",
"file_load.built": "Costruite finora: {built} di {total} feature.",
"file_load.converting": "Conversione del solido {done} di {total}",
"file_load.inserting": "Inserimento della geometria",
@@ -856,19 +873,57 @@
"help.mit": "Licenza MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenze di terze parti",
"home.back": "Torna a {name}",
"home.crashes": "Rapporti di arresto anomalo",
"home.design_workspace": "Progettazione",
"home.documentation": "Documentazione",
"home.export": "Esporta",
"home.export_hint": "Scegli un formato per il progetto attuale.",
"home.export_json_hint": "Progetto modificabile e cronologia dei comandi",
"home.export_mesh_hint": "Mesh per la stampa o la condivisione",
"home.export_step_hint": "Geometria solida precisa",
"home.import": "Importa…",
"home.import_hint": "STEP, IGES o mesh in un nuovo progetto",
"home.in_this_browser": "in questo browser",
"home.missing": "non presente",
"home.month_01": "gen",
"home.month_02": "feb",
"home.month_03": "mar",
"home.month_04": "apr",
"home.month_05": "mag",
"home.month_06": "giu",
"home.month_07": "lug",
"home.month_08": "ago",
"home.month_09": "set",
"home.month_10": "ott",
"home.month_11": "nov",
"home.month_12": "dic",
"home.new_inches": "Nuovo progetto in pollici",
"home.new_inches_hint": "Pollici · si apre in Progettazione",
"home.new_mm": "Nuovo progetto",
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
"home.open": "Apri…",
"home.opened_days_ago": "Aperto {count} giorni fa",
"home.opened_hours_ago": "Aperto {count} h fa",
"home.opened_just_now": "Appena aperto",
"home.opened_minutes_ago": "Aperto {count} min fa",
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
"home.open_now": "Apri ora",
"home.opened_days_ago": "{count} giorni fa",
"home.opened_hours_ago": "{count} h fa",
"home.opened_just_now": "Adesso",
"home.opened_minutes_ago": "{count} min fa",
"home.opening": "apertura…",
"home.properties": "Proprietà del progetto",
"home.recent": "Recenti",
"home.recent_empty": "I progetti che apri o salvi vengono visualizzati qui.",
"home.recovery_count": "Progetti recuperabili: {count}",
"home.recovery_empty": "Nessun progetto recuperabile.",
"home.recovery_hint": "SolidCraft si è chiuso prima del salvataggio di questi progetti.",
"home.report_issue": "Segnala un problema",
"home.samples": "Esempi",
"home.settings": "Impostazioni",
"home.short_date": "{day} {month}",
"home.shortcuts": "Scorciatoie da tastiera",
"home.start_hint": "Crea un progetto, apri un file recente o esplora un esempio.",
"home.whats_new": "Novità",
"home.yesterday": "Ieri",
"install.button_tip": "Installa SolidCraft come applicazione: si apre in una finestra dedicata e funziona offline",
"install.dock_1": "Scegli File › Aggiungi al Dock.",
"install.dock_2": "Fai clic su Aggiungi.",
@@ -946,6 +1001,16 @@
"pattern.direction_1": "Direzione 1",
"pattern.direction_2": "Direzione 2",
"pattern.flip_direction": "Inverti direzione",
"perf.click_for_details": "Fai clic per i dettagli: grafico dei tempi, istogramma e ciò che la GPU ha disegnato",
"perf.draw_calls": "Chiamate di disegno",
"perf.line_segments": "Segmenti",
"perf.low_1": "1% più lento",
"perf.renderer": "Renderer",
"perf.scene_build": "Costruzione scena",
"perf.triangles": "Triangoli",
"perf.ui_time": "Tempo UI",
"perf.viewport": "Vista",
"perf.worst": "peggiore",
"pick a hole to seat it, or OK places it at the origin": "seleziona un foro in cui inserirlo, oppure premi OK per posizionarlo nell'origine",
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
"prefs.cpu": "CPU (software)",
@@ -1021,7 +1086,8 @@
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
"search.valid": "Valido",
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista. Quando è attivo, la vista viene ridisegnata continuamente.",
"settings.frame_rate_details_note": "Aggiunge un grafico dei tempi degli ultimi fotogrammi con i triangoli disegnati sopra, un istogramma dei tempi dei fotogrammi e ciò che la GPU ha disegnato. Fai clic sulla frequenza nella vista per mostrarli o nasconderli.",
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
"settings.msaa_applies_on_restart": "L'antialiasing verrà applicato al prossimo avvio di SolidCraft",
@@ -1082,7 +1148,106 @@
"trace.stopped_by_contact": "Termina al campione {sample}: {a} attraverserebbe {b}",
"trace.stopped_by_loop": "Termina al campione {sample}: la catena chiusa che passa per {joint} non può chiudersi ulteriormente",
"ui.1_degree_of_freedom": "1 grado di libertà",
"ui.3d_mouse_axes_reassigned": "Gli assi sono riassegnati nelle Preferenze, e questo vale anche dopo questa calibrazione.",
"ui.3d_mouse_button": "Pulsante {number}",
"ui.3d_mouse_button_nothing": "Niente",
"ui.3d_mouse_button_settings": "Impostazioni del mouse 3D",
"ui.3d_mouse_buttons": "Pulsanti",
"ui.3d_mouse_buttons_hint": "Premi un pulsante del tuo mouse 3D per trovarlo qui, poi scegli cosa fa.",
"ui.3d_mouse_buttons_no_device": "Collega un mouse 3D per scegliere cosa fanno i suoi pulsanti.",
"ui.3d_mouse_buttons_reset": "Ripristina i pulsanti predefiniti",
"ui.3d_mouse_calibrate": "Calibra…",
"ui.3d_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
"ui.3d_mouse_calibrate_title": "Calibra mouse 3D",
"ui.3d_mouse_calibrate_wait": "Muovi il mouse 3D una volta così SolidCraft lo vede.",
"ui.3d_mouse_calibrated": "Calibrato",
"ui.3d_mouse_calibration": "Calibrazione del mouse 3D",
"ui.3d_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
"ui.3d_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
"ui.3d_mouse_circles_help": "I cerchi mostrano ciò che il dispositivo invia dopo la calibrazione. Il modello di prova mostra cosa ne fa la vista, dopo inversione, velocità e assegnazione degli assi.",
"ui.3d_mouse_dead_zone": "Zona morta",
"ui.3d_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D ma non ha il permesso di leggerlo. La regola udev è nelle note di configurazione del mouse 3D.",
"ui.3d_mouse_denied_short": "Trovato, ma non leggibile",
"ui.3d_mouse_driven_by": "Quale movimento calibrato del dispositivo comanda questo asse. Se ne scegli uno già usato da un altro asse, i due si scambiano.",
"ui.3d_mouse_driven_by_label": "comandato da",
"ui.3d_mouse_driver": "Driver del mouse 3D",
"ui.3d_mouse_driver_auto": "Automatico",
"ui.3d_mouse_driver_direct": "Diretto (hidraw)",
"ui.3d_mouse_driver_fallback": "Per ora legge tramite spacenavd: il dispositivo stesso non si è potuto aprire.",
"ui.3d_mouse_driver_only_direct": "Lettura diretta del dispositivo; spacenavd non è in esecuzione",
"ui.3d_mouse_driver_only_spacenavd": "Lettura tramite spacenavd, l'unica possibilità disponibile",
"ui.3d_mouse_forget": "Dimentica calibrazione",
"ui.3d_mouse_ghost_left_viewport": "Vista a sinistra",
"ui.3d_mouse_ghost_left_window": "Finestra a sinistra",
"ui.3d_mouse_ghost_position": "Posizione",
"ui.3d_mouse_ghost_right_viewport": "Vista a destra",
"ui.3d_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
"ui.3d_mouse_kind_cap": "Ritorno al centro",
"ui.3d_mouse_kind_question": "Che tipo di mouse 3D è?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "Il valore resta finché tieni, come in un dispositivo di tipo «Ritorno al centro».",
"ui.3d_mouse_looks_like_trackball": "Il valore torna indietro appena smetti di muovere, come in una trackball.",
"ui.3d_mouse_motion_rotation_x": "Rotazione X",
"ui.3d_mouse_motion_rotation_y": "Rotazione Y",
"ui.3d_mouse_motion_rotation_z": "Rotazione Z",
"ui.3d_mouse_motion_translation_x": "Traslazione X",
"ui.3d_mouse_motion_translation_y": "Traslazione Y",
"ui.3d_mouse_motion_translation_z": "Traslazione Z",
"ui.3d_mouse_navigate": "Naviga con il mouse 3D",
"ui.3d_mouse_none": "Nessun mouse 3D trovato",
"ui.3d_mouse_not_calibrated": "Non calibrato",
"ui.3d_mouse_offer": "Calibrare il mouse 3D corregge gli assi scambiati o invertiti e imposta velocità e sensibilità.",
"ui.3d_mouse_offer_never": "Non mostrare più",
"ui.3d_mouse_offer_swap": "Passa a Hidraw (driver diretto)",
"ui.3d_mouse_reading": "Lettura",
"ui.3d_mouse_reset_axes": "Ripristina assegnazione assi",
"ui.3d_mouse_reset_view": "Ripristina vista",
"ui.3d_mouse_restore": "Ripristina impostazioni mouse 3D",
"ui.3d_mouse_restore_ask": "Riportare le impostazioni del mouse 3D ai valori predefiniti? Calibrazione e assegnazione dei pulsanti restano.",
"ui.3d_mouse_section": "Mouse 3D",
"ui.3d_mouse_show_in_view": "Mostra il mouse 3D nella vista 3D",
"ui.3d_mouse_show_in_view_tip": "Un'immagine del tuo mouse 3D nell'angolo in basso a sinistra, che si muove quando lo muovi.",
"ui.3d_mouse_skip": "Il mio dispositivo non può farlo",
"ui.3d_mouse_skip_tip": "Salta questo movimento in entrambi i versi; la calibrazione lo esclude.",
"ui.3d_mouse_speed": "Velocità del mouse 3D",
"ui.3d_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
"ui.3d_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
"ui.3d_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
"ui.3d_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
"ui.3d_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
"ui.3d_mouse_step_rest_trackball": "Togli la mano dalla sfera e lasciala ferma.",
"ui.3d_mouse_step_ring_clockwise": "Gira la ghiera di scorrimento di qualche scatto in senso orario.",
"ui.3d_mouse_step_ring_counterclockwise": "Gira la ghiera di scorrimento di qualche scatto in senso antiorario.",
"ui.3d_mouse_step_roll_away": "Fai rotolare la sfera lontano da te qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_roll_left": "Fai rotolare la sfera verso sinistra qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_roll_right": "Fai rotolare la sfera verso destra qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_roll_toward": "Fai rotolare la sfera verso di te qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
"ui.3d_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
"ui.3d_mouse_step_spin_clockwise": "Ruota la sfera in senso orario qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_spin_counterclockwise": "Ruota la sfera in senso antiorario qualche volta, poi lasciala fermare.",
"ui.3d_mouse_step_tilt_away": "Inclina la parte superiore della manopola lontano da te (il bordo più vicino sale), tienila, poi lasciala.",
"ui.3d_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
"ui.3d_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
"ui.3d_mouse_step_tilt_toward": "Inclina la parte superiore della manopola verso di te (il bordo più vicino scende), tienila, poi lasciala.",
"ui.3d_mouse_step_touch": "Appoggia la mano sulla manopola senza spingerla.",
"ui.3d_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
"ui.3d_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
"ui.3d_mouse_switch_cap": "Calibra come ritorno al centro",
"ui.3d_mouse_switch_trackball": "Calibra come trackball",
"ui.3d_mouse_view_axis": "Asse della vista",
"ui.3d_mouse_view_off": "Disattivato",
"ui.3d_mouse_view_off_tip": "Il profilo del driver del mouse 3D disattiva questo asse, quindi il suo movimento non muove la vista. Se qui scegli il movimento di un altro asse, i due si scambiano e quel movimento smette di muovere la vista.",
"ui.3d_mouse_view_orbit_sideways": "Orbita sinistra / destra",
"ui.3d_mouse_view_orbit_up_down": "Orbita su / giù",
"ui.3d_mouse_view_pan_sideways": "Panoramica sinistra / destra",
"ui.3d_mouse_view_pan_up_down": "Panoramica su / giù",
"ui.3d_mouse_view_roll": "Rollio",
"ui.3d_mouse_view_unmapped": "Asse {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "In attesa di movimento",
"ui.3d_mouse_wiggle": "Se ne hai uno collegato, muovilo un po'.",
"ui.3d_sketch": "Schizzo 3D",
"ui.activate": "Attiva",
"ui.active_component": "Componente attivo",
@@ -1092,6 +1257,7 @@
"ui.agent_chat_add": "Allega file",
"ui.agent_chat_add_file": "Scegli un file…",
"ui.agent_chat_allow": "Consenti",
"ui.agent_chat_allow_once": "Consenti una volta",
"ui.agent_chat_allowed": "Consentito",
"ui.agent_chat_answered": "Risposta: {option}",
"ui.agent_chat_approval": "Consentire questa azione all'agente?",
@@ -1143,12 +1309,16 @@
"ui.agent_chat_files_left_out": "Un messaggio può contenere al massimo {max} file; esclusi: {files}",
"ui.agent_chat_files_loading": "In attesa della lettura dei file",
"ui.agent_chat_files_max": "Al massimo {max} file per messaggio",
"ui.agent_chat_flatpak_none_found": "Nessun agente di programmazione trovato. Per impostazione predefinita, la sandbox Flatpak non vede gli agenti installati nella tua cartella home e non ha accesso alla rete. Per consentire Claude Code e il suo adattatore ACP installati con nvm o in ~/.local/bin, esegui questo comando in un terminale, poi riavvia SolidCraft. Concede l’accesso ai programmi e ai file di accesso elencati, oltre all’accesso alla rete:",
"ui.agent_chat_import_failed": "{name}: importazione non riuscita: {error}",
"ui.agent_chat_intro": "Conversa con un agente di programmazione che già usi o con un endpoint di modello a tua scelta. Opera sul progetto aperto tramite gli strumenti di SolidCraft: le modifiche sono visibili mentre vengono eseguite e si annullano in un solo passo per messaggio. Le azioni consentite senza conferma si impostano in Preferenze > IA.",
"ui.agent_chat_key_in_env": "chiave impostata",
"ui.agent_chat_left": "Sinistra",
"ui.agent_chat_limit": "L'agente ha raggiunto il limite di {limit} modifiche per messaggio. Invia un altro messaggio per farlo proseguire.",
"ui.agent_chat_minimize": "Riduci a una striscia",
"ui.agent_chat_mode_all": "Tutto",
"ui.agent_chat_mode_ask": "Chiedi",
"ui.agent_chat_mode_edits": "Modifiche",
"ui.agent_chat_model_tip": "Il modello usato dall'agente. La scelta viene memorizzata per questo agente.",
"ui.agent_chat_new": "Nuova chat",
"ui.agent_chat_new_confirm": "Avviare una nuova chat? Quella corrente verrà cancellata.",
@@ -1161,6 +1331,7 @@
"ui.agent_chat_none_found_short": "Nessun agente trovato",
"ui.agent_chat_not_installed": "Non installato",
"ui.agent_chat_not_signed_in": "accesso non effettuato",
"ui.agent_chat_open_settings": "Apri preferenze",
"ui.agent_chat_own_window": "Finestra separata",
"ui.agent_chat_paste_picture": "Incolla un'immagine",
"ui.agent_chat_permission": "L'agente chiede di eseguire: {title}",
@@ -1185,6 +1356,8 @@
"ui.agent_chat_send": "Invia",
"ui.agent_chat_send_now": "Invia subito",
"ui.agent_chat_send_now_tip": "Interrompi il passo corrente dell'agente e invia questo messaggio come successivo (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft comunica con un agente di programmazione installato su questo computer. Scegline uno in Preferenze › IA.",
"ui.agent_chat_setup_title": "Configura un assistente",
"ui.agent_chat_side": "Lato della chat con l'agente",
"ui.agent_chat_signed_in": "accesso effettuato",
"ui.agent_chat_state_auth": "Accesso richiesto",
@@ -1201,6 +1374,7 @@
"ui.agent_chat_undo_label": "Chat con l'agente: {message}",
"ui.agent_chat_undone": "(annullato)",
"ui.agent_chat_view_attached": "Vista allegata",
"ui.agent_chat_waiting": "In attesa della tua risposta",
"ui.agent_cursor": "Cursore dell'agente",
"ui.agent_cursor_speed": "Velocità del cursore dell'agente",
"ui.ai_add": "Aggiungi",
@@ -1258,7 +1432,6 @@
"ui.ai_permissions": "Autorizzazioni",
"ui.ai_preset": "Preimpostazione",
"ui.ai_remove": "Rimuovi",
"ui.ai_request_limit": "arrestato dopo 24 richieste per un messaggio; invia un altro messaggio per proseguire",
"ui.ai_reset": "Reimposta",
"ui.ai_settings": "Impostazioni IA…",
"ui.ai_show_features": "Mostra funzioni IA",
@@ -1303,6 +1476,7 @@
"ui.blocked": "Bloccato",
"ui.bodies": "Corpi",
"ui.bodies_all_if_none": "Corpi (tutti se nessuno)",
"ui.bottom": "Inferiore",
"ui.box": "Parallelepipedo",
"ui.break": "Spezza",
"ui.browser_bodies_one": "{count} corpo",
@@ -1441,6 +1615,7 @@
"ui.flip": "Inverti",
"ui.format": "Formato",
"ui.frame_rate": "Frequenza dei fotogrammi",
"ui.frame_rate_details": "Dettagli della frequenza dei fotogrammi",
"ui.front": "Frontale",
"ui.fully_constrained": "Completamente vincolato",
"ui.function": "Funzione",
@@ -1455,6 +1630,7 @@
"ui.grid_follows_view_hint": "Le viste laterali mostrano la griglia sul piano verticale rivolto verso di esse anziché sul piano di appoggio",
"ui.ground_shadow": "Ombra sul piano di appoggio",
"ui.groupSelected": "Raggruppa selezionati",
"ui.hatch": "Tratteggio",
"ui.heading_add_ins": "COMPONENTI AGGIUNTIVI",
"ui.heading_assemble": "ASSEMBLA",
"ui.heading_box": "PARALLELEPIPEDO",
@@ -1528,6 +1704,11 @@
"ui.hideOrigin": "Nascondi origine",
"ui.hidePlanes": "Nascondi piani",
"ui.hideProfile": "Nascondi profilo",
"ui.history_back": "Indietro di un passaggio",
"ui.history_below_browser": "Sotto il browser",
"ui.history_first": "Vai all’inizio",
"ui.history_forward": "Avanti di un passaggio",
"ui.history_last": "Vai alla fine",
"ui.history_title": "Cronologia",
"ui.hole": "Foro",
"ui.hole_tap_type": "Tipo di maschiatura del foro",
@@ -1545,6 +1726,8 @@
"ui.joints": "Giunti",
"ui.keyboard_shortcuts": "Scorciatoie da tastiera",
"ui.language": "Lingua",
"ui.layout_compact": "Compatta",
"ui.layout_floating": "Flottante",
"ui.length_decimals": "Decimali delle lunghezze",
"ui.light": "Chiaro",
"ui.line": "Linea",
@@ -1682,6 +1865,7 @@
"ui.reverse_zoom_direction": "Inverti direzione dello zoom",
"ui.revolve": "Rivoluzione",
"ui.ribbon_exit": "Esci",
"ui.ribbon_labels": "Mostra il testo sotto le icone della barra multifunzione",
"ui.ribbon_pin_footer": "I comandi fissati compaiono sulla barra multifunzione. Passa sopra una riga per fissarla o rimuoverla.",
"ui.ribbon_workspace": "Area di lavoro",
"ui.right": "Destra",
@@ -1737,34 +1921,6 @@
"ui.smooth_wheel_zoom": "Zoom fluido con rotellina",
"ui.smooth_wheel_zoom_tip": "Disattiva per applicare immediatamente ogni evento della rotellina, senza coda di interpolazione",
"ui.snap": "Aggancio",
"ui.space_mouse_calibrate": "Calibra…",
"ui.space_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
"ui.space_mouse_calibrate_title": "Calibra mouse 3D",
"ui.space_mouse_calibrated": "Calibrato",
"ui.space_mouse_calibration": "Calibrazione del mouse 3D",
"ui.space_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
"ui.space_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
"ui.space_mouse_dead_zone": "Zona morta",
"ui.space_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
"ui.space_mouse_forget": "Dimentica calibrazione",
"ui.space_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
"ui.space_mouse_navigate": "Naviga con SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Nessun mouse 3D trovato",
"ui.space_mouse_not_calibrated": "Non calibrato",
"ui.space_mouse_reading": "Lettura",
"ui.space_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
"ui.space_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
"ui.space_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
"ui.space_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
"ui.space_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
"ui.space_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
"ui.space_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
"ui.space_mouse_step_tilt_back": "Inclina la manopola all'indietro (bordo anteriore in alto), tienila, poi lasciala.",
"ui.space_mouse_step_tilt_forward": "Inclina la manopola in avanti (bordo anteriore in basso), tienila, poi lasciala.",
"ui.space_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
"ui.space_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
"ui.space_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
"ui.space_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
"ui.spacing": "Spaziatura",
"ui.spacing_2": "Spaziatura 2",
"ui.sphere": "Sfera",
@@ -1836,6 +1992,7 @@
"ui.workspace_design_tabs": "Modello · Superficie · Assembla · Inserisci · Ispeziona · Gestisci · Vista",
"ui.workspace_drawing_description": "Disegni 2D con viste, quote e cartigli",
"ui.workspace_footer": "Ogni area di lavoro opera sullo stesso progetto, cronologia e selezione; cambiano solo le schede della barra multifunzione. Il selettore è bloccato durante la modifica di uno schizzo o di una feature.",
"ui.workspace_layout": "Disposizione dello spazio di lavoro",
"ui.workspace_manufacture_description": "Slicing, percorsi utensile e impostazione",
"ui.workspace_planned": "Pianificata",
"ui.workspace_render_description": "Anteprima del prodotto, impostazioni della scena e immagini renderizzate",
+1288 -41
View File
File diff suppressed because it is too large Load Diff
+612 -38
View File
File diff suppressed because it is too large Load Diff
+176 -36
View File
@@ -72,6 +72,7 @@
"Choose…": "Pilih…",
"Circular": "Bulat",
"Circumference": "Lilitan",
"Clockwise": "Ikut arah jam",
"Close draft": "Tutup draf",
"Collisions": "Perlanggaran",
"Colour": "Warna",
@@ -167,6 +168,7 @@
"HISTORY": "SEJARAH",
"Height": "Ketinggian",
"Hidden edges": "Tepi tersembunyi",
"History layout": "Susun atur sejarah",
"Holes": "Lubang",
"Holes (edges on them)": "Lubang (tepi lubang)",
"Home view": "Pandangan utama",
@@ -232,7 +234,7 @@
"No crash reports.": "Tiada laporan ranap.",
"No matching appearance presets.": "Tiada pratetap penampilan yang sepadan.",
"No matching appearances": "Tiada penampilan yang sepadan",
"No visible bodies to export.": "Tiada jasad kelihatan untuk dieksport.",
"No visible bodies or sketches to export.": "Tiada jasad atau lakaran kelihatan untuk dieksport.",
"None": "Tiada",
"Not available yet": "Belum tersedia",
"Notch": "Takuk",
@@ -321,6 +323,7 @@
"Search commands": "Cari arahan",
"Search this library": "Cari dalam koleksi ini",
"Section": "Keratan",
"Section position": "Kedudukan keratan",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Pilih Lain: tahan butang kiri pada model tanpa bergerak (atau klik kanan › Pilih Lain…) untuk senarai objek di bawah kursor. Klik tempat sama untuk objek seterusnya. Klik dua kali butang tengah untuk ke Utama (Tetapan › Navigasi).",
"Select bodies (a face picks its body)": "Pilih jasad (klik muka untuk pilih jasadnya)",
"Select bodies or faces instead": "Pilih jasad atau muka",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Papar Kursor Ejen",
"Show in folder": "Papar dalam folder",
"Show the pointer an AI agent moves (#35)": "Papar penuding yang digerakkan oleh ejen AI (#35)",
"Show tool at cursor": "Tunjukkan alat di sebelah kursor",
"Signed axes": "Paksi bertanda",
"Size": "Saiz",
"Sketch constraints": "Kekangan lakaran",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft ditutup sebelum reka bentuk disimpan. Buka simpanan automatik terakhirnya.",
"Specialise": "Khususkan",
"Speed": "Kelajuan",
"Square": "Segi empat sama",
"Stand it up": "Tegakkan",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: suku putaran pada X, satu ciri Alih yang boleh dibatalkan.",
"Stand this part up?": "Tegakkan bahagian ini?",
"Start angle": "Sudut mula",
"Start bulge": "Bonjolan permulaan",
"Start continuity": "Keselanjaran permulaan",
"Step": "Langkah",
@@ -411,6 +417,7 @@
"angle, height": "sudut, ketinggian",
"appearance.dielectric_ior": "IOR Dielektrik",
"appearance.metallic": "Sifat Logam",
"appearance.only_available": "Rupa yang tersedia sahaja",
"appearance.pbr_help": "Lihat dalam Render atau PNG produk. Tiada pembiasan atau penembusan cahaya.",
"appearance.pbr_opaque": "PBR Legap",
"appearance.roughness": "Kekasaran",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Sisip STEP",
"cmd.file_save_mesh": "Simpan sebagai Mesh",
"cmd.file_slicers": "Penghiris Terpasang",
"cmd.frame_member": "Anggota Rangka",
"cmd.frame_profile_insert": "Sisipkan Profil",
"cmd.frame_profile_list": "Pustaka Profil",
"cmd.frame_profile_properties": "Sifat Profil",
"cmd.inspect_accessibility": "Analisis Kebolehcapaian",
"cmd.inspect_center_of_mass": "Pusat Jisim",
"cmd.inspect_clearance": "Kelegaan",
@@ -568,6 +579,7 @@
"cmd.material_create": "Bahan Fizikal Baharu",
"cmd.material_delete": "Padam Bahan Fizikal",
"cmd.material_edit": "Sunting Bahan Fizikal",
"cmd.material_inspect": "Periksa Bahan Fizikal",
"cmd.material_list": "Senarai Bahan",
"cmd.material_properties_directional": "Modulus Keanjalan Berarah",
"cmd.material_properties_validate": "Sahkan Data Bahan Fizikal",
@@ -831,7 +843,7 @@
"dxf.progress.projection": "Mengunjurkan geometri (3/5)",
"dxf.progress.write": "Menulis DXF (5/5)",
"dxf.tolerance": "Toleransi lengkung",
"dxf.tolerance_help": "Jasad kelihatan sahaja. Lengkung guna segmen lurus. Toleransi kecil lebih halus tetapi guna lebih masa dan memori.",
"dxf.tolerance_help": "Jasad kelihatan dan lakaran yang ditunjukkan sahaja. Lengkung guna segmen lurus. Toleransi kecil lebih halus tetapi guna lebih masa dan memori.",
"file_load.built": "Dibina setakat ini: {built} daripada {total} ciri.",
"file_load.converting": "Menukar pepejal {done} daripada {total}",
"file_load.inserting": "Menyisip geometri",
@@ -856,19 +868,57 @@
"help.mit": "Lesen MIT",
"help.notice": "NOTIS",
"help.third_party": "Lesen pihak ketiga",
"home.back": "Kembali ke {name}",
"home.crashes": "Laporan ranap",
"home.design_workspace": "Reka Bentuk",
"home.documentation": "Dokumentasi",
"home.export": "Eksport",
"home.export_hint": "Pilih format untuk reka bentuk semasa.",
"home.export_json_hint": "Reka bentuk boleh sunting dan sejarah perintah",
"home.export_mesh_hint": "Jejaring untuk pencetakan atau perkongsian",
"home.export_step_hint": "Geometri pepejal yang tepat",
"home.import": "Import…",
"home.import_hint": "STEP, IGES atau jejaring ke dalam reka bentuk baharu",
"home.in_this_browser": "dalam pelayar ini",
"home.missing": "tiada",
"home.month_01": "Jan",
"home.month_02": "Feb",
"home.month_03": "Mac",
"home.month_04": "Apr",
"home.month_05": "Mei",
"home.month_06": "Jun",
"home.month_07": "Jul",
"home.month_08": "Ogo",
"home.month_09": "Sep",
"home.month_10": "Okt",
"home.month_11": "Nov",
"home.month_12": "Dis",
"home.new_inches": "Reka bentuk baharu dalam inci",
"home.new_inches_hint": "Inci · dibuka dalam Reka Bentuk",
"home.new_mm": "Reka bentuk baharu",
"home.new_mm_hint": "Milimeter · dibuka dalam Reka Bentuk",
"home.open": "Buka…",
"home.opened_days_ago": "Dibuka {count} hari lalu",
"home.opened_hours_ago": "Dibuka {count} jam lalu",
"home.opened_just_now": "Baru dibuka",
"home.opened_minutes_ago": "Dibuka {count} minit lalu",
"home.open_hint": "Reka bentuk .solidcraft ({shortcut})",
"home.open_now": "Buka sekarang",
"home.opened_days_ago": "{count} hari lalu",
"home.opened_hours_ago": "{count} jam lalu",
"home.opened_just_now": "Baru sahaja",
"home.opened_minutes_ago": "{count} minit lalu",
"home.opening": "membuka…",
"home.properties": "Sifat reka bentuk",
"home.recent": "Terkini",
"home.recent_empty": "Reka bentuk yang anda buka atau simpan dipaparkan di sini.",
"home.recovery_count": "Reka bentuk boleh dipulihkan: {count}",
"home.recovery_empty": "Tiada reka bentuk yang boleh dipulihkan.",
"home.recovery_hint": "SolidCraft ditutup sebelum reka bentuk ini disimpan.",
"home.report_issue": "Laporkan Isu",
"home.samples": "Contoh",
"home.settings": "Tetapan",
"home.short_date": "{day} {month}",
"home.shortcuts": "Pintasan papan kekunci",
"home.start_hint": "Cipta reka bentuk, buka fail terkini atau terokai contoh.",
"home.whats_new": "Apa yang baharu",
"home.yesterday": "Semalam",
"install.button_tip": "Pasang SolidCraft: tetingkap sendiri dan boleh guna di luar talian",
"install.dock_1": "Pilih File › Add to Dock.",
"install.dock_2": "Klik Add.",
@@ -1021,7 +1071,7 @@
"search.try": "Taip nama arahan, ciri dalam reka bentuk ini, atau parameter. Untuk menjalankan arahan dengan nilai, taip ID-nya dan objek JSON: {example}",
"search.valid": "Sah",
"settings.display_quality_note": "Tahap halus melicinkan muka dan tepi melengkung, tetapi mesh lebih lambat disiapkan. Hanya paparan berubah, bukan reka bentuk atau eksport.",
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah. Jika diaktifkan, pandangan dilukis semula tanpa henti.",
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah.",
"settings.measure_axis_distances": "Papar jarak X/Y/Z",
"settings.measure_axis_distances_help": "Papar jarak paksi dunia X/Y/Z bersama jarak keseluruhan semasa Ukur dibuka.",
"settings.msaa_applies_on_restart": "Antialias digunakan apabila SolidCraft dimulakan semula",
@@ -1082,7 +1132,106 @@
"trace.stopped_by_contact": "Berakhir pada sampel {sample}: {a} akan menembusi {b}",
"trace.stopped_by_loop": "Berakhir pada sampel {sample}: gelung melalui {joint} tidak lagi dapat ditutup",
"ui.1_degree_of_freedom": "1 darjah kebebasan",
"ui.3d_mouse_axes_reassigned": "Paksi ditetapkan semula dalam Keutamaan, dan itu masih terpakai di atas penentukuran ini.",
"ui.3d_mouse_button": "Butang {number}",
"ui.3d_mouse_button_nothing": "Tiada",
"ui.3d_mouse_button_settings": "Tetapan tetikus 3D",
"ui.3d_mouse_buttons": "Butang",
"ui.3d_mouse_buttons_hint": "Tekan butang pada tetikus 3D anda untuk mencarinya di sini, kemudian pilih fungsinya.",
"ui.3d_mouse_buttons_no_device": "Sambungkan tetikus 3D untuk menetapkan fungsi butangnya.",
"ui.3d_mouse_buttons_reset": "Pulihkan butang lalai",
"ui.3d_mouse_calibrate": "Tentukur…",
"ui.3d_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
"ui.3d_mouse_calibrate_title": "Tentukur tetikus 3D",
"ui.3d_mouse_calibrate_wait": "Gerakkan tetikus 3D sekali supaya SolidCraft dapat melihatnya.",
"ui.3d_mouse_calibrated": "Telah ditentukur",
"ui.3d_mouse_calibration": "Penentukuran tetikus 3D",
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
"ui.3d_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
"ui.3d_mouse_circles_help": "Bulatan menunjukkan apa yang dihantar peranti selepas penentukuran. Model ujian menunjukkan apa yang dilakukan paparan dengannya, selepas songsangan, kelajuan dan penetapan paksi.",
"ui.3d_mouse_dead_zone": "Zon mati",
"ui.3d_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
"ui.3d_mouse_denied": "SolidCraft nampak tetikus 3D tetapi tidak dibenarkan membacanya. Lihat nota persediaan tetikus 3D untuk peraturan udev.",
"ui.3d_mouse_denied_short": "Dijumpai, tetapi tidak boleh dibaca",
"ui.3d_mouse_driven_by": "Gerakan peranti yang telah ditentukur yang memacu paksi ini. Memilih gerakan yang sudah digunakan paksi lain akan menukar kedua-duanya.",
"ui.3d_mouse_driven_by_label": "dipacu oleh",
"ui.3d_mouse_driver": "Pemacu tetikus 3D",
"ui.3d_mouse_driver_auto": "Automatik",
"ui.3d_mouse_driver_direct": "Terus (hidraw)",
"ui.3d_mouse_driver_fallback": "Buat masa ini dibaca melalui spacenavd: peranti itu sendiri tidak dapat dibuka.",
"ui.3d_mouse_driver_only_direct": "Membaca peranti secara terus; spacenavd tidak berjalan",
"ui.3d_mouse_driver_only_spacenavd": "Membaca melalui spacenavd, satu-satunya cara yang ada",
"ui.3d_mouse_forget": "Lupakan penentukuran",
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
"ui.3d_mouse_ghost_left_window": "Tetingkap kiri",
"ui.3d_mouse_ghost_position": "Kedudukan",
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
"ui.3d_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
"ui.3d_mouse_kind_cap": "Kembali ke tengah",
"ui.3d_mouse_kind_question": "Apakah jenis tetikus 3D ini?",
"ui.3d_mouse_kind_trackball": "Bola jejak",
"ui.3d_mouse_looks_like_cap": "Bacaan kekal selagi anda menahannya, seperti peranti jenis “Kembali ke tengah”.",
"ui.3d_mouse_looks_like_trackball": "Bacaan kembali sebaik sahaja anda berhenti bergerak, seperti bola jejak.",
"ui.3d_mouse_motion_rotation_x": "Putaran X",
"ui.3d_mouse_motion_rotation_y": "Putaran Y",
"ui.3d_mouse_motion_rotation_z": "Putaran Z",
"ui.3d_mouse_motion_translation_x": "Anjakan X",
"ui.3d_mouse_motion_translation_y": "Anjakan Y",
"ui.3d_mouse_motion_translation_z": "Anjakan Z",
"ui.3d_mouse_navigate": "Navigasi dengan tetikus 3D",
"ui.3d_mouse_none": "Tiada tetikus 3D ditemui",
"ui.3d_mouse_not_calibrated": "Belum ditentukur",
"ui.3d_mouse_offer": "Menentukur tetikus 3D membetulkan paksi yang bertukar atau terbalik dan menetapkan kelajuan serta rasanya.",
"ui.3d_mouse_offer_never": "Jangan tunjuk lagi",
"ui.3d_mouse_offer_swap": "Tukar ke Hidraw (pemacu terus)",
"ui.3d_mouse_reading": "Bacaan",
"ui.3d_mouse_reset_axes": "Tetapkan semula penetapan paksi",
"ui.3d_mouse_reset_view": "Set semula paparan",
"ui.3d_mouse_restore": "Pulihkan lalai tetikus 3D",
"ui.3d_mouse_restore_ask": "Tetapkan semula tetapan tetikus 3D kepada lalai? Penentukuran dan pemetaan butang kekal.",
"ui.3d_mouse_section": "Tetikus 3D",
"ui.3d_mouse_show_in_view": "Tunjukkan tetikus 3D dalam paparan 3D",
"ui.3d_mouse_show_in_view_tip": "Gambar tetikus 3D anda di sudut kiri bawah, yang bergerak apabila anda menggerakkannya.",
"ui.3d_mouse_skip": "Peranti saya tidak boleh buat ini",
"ui.3d_mouse_skip_tip": "Langkau gerakan ini kedua-dua arah; penentukuran tidak menggunakannya.",
"ui.3d_mouse_speed": "Kelajuan tetikus 3D",
"ui.3d_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
"ui.3d_mouse_step_rest_trackball": "Alihkan tangan anda daripada bola dan biarkan ia diam.",
"ui.3d_mouse_step_ring_clockwise": "Putar gelang tatal ikut arah jam beberapa klik.",
"ui.3d_mouse_step_ring_counterclockwise": "Putar gelang tatal lawan arah jam beberapa klik.",
"ui.3d_mouse_step_roll_away": "Gulingkan bola menjauhi anda beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_roll_left": "Gulingkan bola ke kiri beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_roll_right": "Gulingkan bola ke kanan beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_roll_toward": "Gulingkan bola ke arah anda beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_spin_clockwise": "Pusing bola ikut arah jam beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_spin_counterclockwise": "Pusing bola lawan arah jam beberapa kali, kemudian biarkan ia berhenti.",
"ui.3d_mouse_step_tilt_away": "Condongkan bahagian atas penutup menjauhi anda (tepi terdekat naik), tahan, kemudian lepaskan.",
"ui.3d_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
"ui.3d_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
"ui.3d_mouse_step_tilt_toward": "Condongkan bahagian atas penutup ke arah anda (tepi terdekat turun), tahan, kemudian lepaskan.",
"ui.3d_mouse_step_touch": "Letakkan tangan anda pada penutup tanpa menolaknya.",
"ui.3d_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
"ui.3d_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
"ui.3d_mouse_switch_cap": "Tentukur sebagai kembali ke tengah",
"ui.3d_mouse_switch_trackball": "Tentukur sebagai bola jejak",
"ui.3d_mouse_view_axis": "Paksi paparan",
"ui.3d_mouse_view_off": "Mati",
"ui.3d_mouse_view_off_tip": "Profil pemacu tetikus 3D mematikan paksi ini, jadi gerakannya tidak menggerakkan paparan. Memilih gerakan paksi lain di sini menukar kedua-duanya, dan gerakan itu tidak lagi menggerakkan paparan.",
"ui.3d_mouse_view_orbit_sideways": "Orbit kiri / kanan",
"ui.3d_mouse_view_orbit_up_down": "Orbit atas / bawah",
"ui.3d_mouse_view_pan_sideways": "Pan kiri / kanan",
"ui.3d_mouse_view_pan_up_down": "Pan atas / bawah",
"ui.3d_mouse_view_roll": "Gelek",
"ui.3d_mouse_view_unmapped": "Paksi {n}",
"ui.3d_mouse_view_zoom": "Zum",
"ui.3d_mouse_waiting": "Menunggu pergerakan",
"ui.3d_mouse_wiggle": "Jika anda ada yang disambungkan, gerakkannya sedikit.",
"ui.3d_sketch": "Lakaran 3D",
"ui.activate": "Aktifkan",
"ui.active_component": "Komponen aktif",
@@ -1092,6 +1241,7 @@
"ui.agent_chat_add": "Lampirkan fail",
"ui.agent_chat_add_file": "Pilih fail…",
"ui.agent_chat_allow": "Benarkan",
"ui.agent_chat_allow_once": "Benarkan sekali",
"ui.agent_chat_allowed": "Dibenarkan",
"ui.agent_chat_answered": "Jawapan: {option}",
"ui.agent_chat_approval": "Benarkan ejen melakukan ini?",
@@ -1143,12 +1293,16 @@
"ui.agent_chat_files_left_out": "Paling banyak {max} fail bersama satu mesej; ditinggalkan: {files}",
"ui.agent_chat_files_loading": "Menunggu fail dibaca",
"ui.agent_chat_files_max": "Paling banyak {max} fail pada satu mesej",
"ui.agent_chat_flatpak_none_found": "Tiada ejen pengekodan. Secara lalai, kotak pasir Flatpak tidak dapat melihat ejen yang dipasang dalam folder rumah anda dan tiada akses rangkaian. Untuk membenarkan Claude Code dan penyesuai ACP-nya yang dipasang melalui nvm atau dalam ~/.local/bin, jalankan arahan ini dalam terminal, kemudian mulakan semula SolidCraft. Ia memberi akses kepada program dan fail log masuk yang disenaraikan, serta akses rangkaian:",
"ui.agent_chat_import_failed": "{name}: import gagal: {error}",
"ui.agent_chat_intro": "Sembang dengan ejen pengekodan biasa anda atau titik akhir model pilihan. Ejen mengubah reka bentuk melalui alat SolidCraft; perubahan dipaparkan terus dan boleh dibatalkan sekali bagi setiap mesej. Tetapkan kebenaran di Tetapan > AI.",
"ui.agent_chat_key_in_env": "kunci tersedia",
"ui.agent_chat_left": "Kiri",
"ui.agent_chat_limit": "Ejen telah mencapai had {limit} perubahan untuk satu mesej. Hantar mesej lain untuk meneruskan.",
"ui.agent_chat_minimize": "Kecilkan kepada bar",
"ui.agent_chat_mode_all": "Semua",
"ui.agent_chat_mode_ask": "Tanya",
"ui.agent_chat_mode_edits": "Suntingan",
"ui.agent_chat_model_tip": "Model yang digunakan oleh ejen. Pilihan anda disimpan untuk ejen ini.",
"ui.agent_chat_new": "Sembang baharu",
"ui.agent_chat_new_confirm": "Mulakan sembang baharu? Sembang semasa akan dikosongkan.",
@@ -1161,6 +1315,7 @@
"ui.agent_chat_none_found_short": "Tiada ejen ditemui",
"ui.agent_chat_not_installed": "Tidak dipasang",
"ui.agent_chat_not_signed_in": "belum log masuk",
"ui.agent_chat_open_settings": "Buka tetapan",
"ui.agent_chat_own_window": "Tetingkap sendiri",
"ui.agent_chat_paste_picture": "Tampal gambar",
"ui.agent_chat_permission": "Benarkan ejen jalankan: {title}",
@@ -1185,6 +1340,8 @@
"ui.agent_chat_send": "Hantar",
"ui.agent_chat_send_now": "Hantar terus",
"ui.agent_chat_send_now_tip": "Hentikan langkah semasa ejen dan hantar mesej ini selepas itu (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft berkomunikasi dengan ejen pengekodan yang dipasang pada komputer ini. Pilih ejen dalam Tetapan › AI.",
"ui.agent_chat_setup_title": "Sediakan pembantu",
"ui.agent_chat_side": "Sisi sembang ejen",
"ui.agent_chat_signed_in": "sudah log masuk",
"ui.agent_chat_state_auth": "Perlu log masuk",
@@ -1201,6 +1358,7 @@
"ui.agent_chat_undo_label": "Sembang ejen: {message}",
"ui.agent_chat_undone": "(dibatalkan)",
"ui.agent_chat_view_attached": "Paparan dilampirkan",
"ui.agent_chat_waiting": "Menunggu anda",
"ui.agent_cursor": "Kursor ejen",
"ui.agent_cursor_speed": "Kelajuan kursor ejen",
"ui.ai_add": "Tambah",
@@ -1258,7 +1416,6 @@
"ui.ai_permissions": "Kebenaran",
"ui.ai_preset": "Pratetap",
"ui.ai_remove": "Buang",
"ui.ai_request_limit": "berhenti selepas 24 permintaan untuk satu mesej; hantar mesej lain untuk meneruskan",
"ui.ai_reset": "Set Semula",
"ui.ai_settings": "Tetapan AI…",
"ui.ai_show_features": "Papar ciri AI",
@@ -1303,6 +1460,7 @@
"ui.blocked": "Disekat",
"ui.bodies": "Jasad",
"ui.bodies_all_if_none": "Jasad (semua jika tiada)",
"ui.bottom": "Bawah",
"ui.box": "Kotak",
"ui.break": "Putuskan",
"ui.browser_bodies_one": "{count} jasad",
@@ -1455,6 +1613,7 @@
"ui.grid_follows_view_hint": "Pandangan sisi guna grid satah tegak yang menghadapnya, bukan satah tanah",
"ui.ground_shadow": "Bayang tanah",
"ui.groupSelected": "Kumpulkan Pilihan",
"ui.hatch": "Lorek",
"ui.heading_add_ins": "TAMBAHAN",
"ui.heading_assemble": "PASANG",
"ui.heading_box": "KOTAK",
@@ -1528,6 +1687,11 @@
"ui.hideOrigin": "Sembunyi Titik Asal",
"ui.hidePlanes": "Sembunyi Satah",
"ui.hideProfile": "Sembunyi Profil",
"ui.history_back": "Undur satu langkah",
"ui.history_below_browser": "Di bawah pelayar",
"ui.history_first": "Pergi ke permulaan",
"ui.history_forward": "Maju satu langkah",
"ui.history_last": "Pergi ke penghujung",
"ui.history_title": "Sejarah",
"ui.hole": "Lubang",
"ui.hole_tap_type": "Jenis Bebenang Lubang",
@@ -1545,6 +1709,8 @@
"ui.joints": "Sendi",
"ui.keyboard_shortcuts": "Pintasan Kekunci",
"ui.language": "Bahasa",
"ui.layout_compact": "Padat",
"ui.layout_floating": "Terapung",
"ui.length_decimals": "Perpuluhan panjang",
"ui.light": "Cerah",
"ui.line": "Garis",
@@ -1682,6 +1848,7 @@
"ui.reverse_zoom_direction": "Songsangkan arah zum",
"ui.revolve": "Putar",
"ui.ribbon_exit": "Keluar",
"ui.ribbon_labels": "Tunjukkan teks di bawah ikon reben",
"ui.ribbon_pin_footer": "Arahan tersemat dipaparkan pada reben. Lalukan tetikus pada baris untuk menyemat atau menyahsemat.",
"ui.ribbon_workspace": "Ruang kerja",
"ui.right": "Kanan",
@@ -1737,34 +1904,6 @@
"ui.smooth_wheel_zoom": "Zum roda lancar",
"ui.smooth_wheel_zoom_tip": "Matikan untuk mengezum serta-merta bagi setiap putaran roda tetikus, tanpa gerakan susulan",
"ui.snap": "Lekap",
"ui.space_mouse_calibrate": "Tentukur…",
"ui.space_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
"ui.space_mouse_calibrate_title": "Tentukur tetikus 3D",
"ui.space_mouse_calibrated": "Telah ditentukur",
"ui.space_mouse_calibration": "Penentukuran tetikus 3D",
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
"ui.space_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
"ui.space_mouse_dead_zone": "Zon mati",
"ui.space_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
"ui.space_mouse_forget": "Lupakan penentukuran",
"ui.space_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Tiada tetikus 3D ditemui",
"ui.space_mouse_not_calibrated": "Belum ditentukur",
"ui.space_mouse_reading": "Bacaan",
"ui.space_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
"ui.space_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
"ui.space_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
"ui.space_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
"ui.space_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
"ui.space_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
"ui.space_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
"ui.space_mouse_step_tilt_back": "Condongkan penutup ke belakang (tepi depan naik), tahan, kemudian lepaskan.",
"ui.space_mouse_step_tilt_forward": "Condongkan penutup ke hadapan (tepi depan turun), tahan, kemudian lepaskan.",
"ui.space_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
"ui.space_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
"ui.space_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
"ui.space_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
"ui.spacing": "Selang",
"ui.spacing_2": "Selang 2",
"ui.sphere": "Sfera",
@@ -1836,6 +1975,7 @@
"ui.workspace_design_tabs": "Model · Permukaan · Pasang · Sisip · Periksa · Urus · Pandangan",
"ui.workspace_drawing_description": "Lukisan 2D dengan pandangan, dimensi dan blok tajuk",
"ui.workspace_footer": "Setiap ruang kerja berfungsi pada reka bentuk, sejarah dan pilihan yang sama; hanya tab reben berubah. Penukar dikunci semasa lakaran atau ciri sedang disunting.",
"ui.workspace_layout": "Susun atur ruang kerja",
"ui.workspace_manufacture_description": "Pemotongan lapisan, laluan alat dan persediaan",
"ui.workspace_planned": "Dirancang",
"ui.workspace_render_description": "Pratonton produk, tetapan adegan dan imej render",
+117 -35
View File
@@ -72,6 +72,7 @@
"Choose…": "Kiezen…",
"Circular": "Cirkelvormig",
"Circumference": "Omtrek",
"Clockwise": "Met de klok mee",
"Close draft": "Concept sluiten",
"Collisions": "Botsingen",
"Colour": "Kleur",
@@ -167,6 +168,7 @@
"HISTORY": "GESCHIEDENIS",
"Height": "Hoogte",
"Hidden edges": "Verborgen randen",
"History layout": "Geschiedenisindeling",
"Holes": "Gaten",
"Holes (edges on them)": "Gaten (randen erop)",
"Home view": "Beginweergave",
@@ -232,7 +234,6 @@
"No crash reports.": "Geen crashrapporten.",
"No matching appearance presets.": "Geen overeenkomende uiterlijkvoorinstellingen.",
"No matching appearances": "Geen overeenkomende uiterlijken",
"No visible bodies to export.": "Geen zichtbare lichamen om te exporteren.",
"None": "Geen",
"Not available yet": "Nog niet beschikbaar",
"Notch": "Inkeping",
@@ -321,6 +322,7 @@
"Search commands": "Opdrachten zoeken",
"Search this library": "Deze bibliotheek doorzoeken",
"Section": "Doorsnede",
"Section position": "Positie van de doorsnede",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Overige selecteren: houd de linkermuisknop stil op het model (of klik met rechts › Overige selecteren…) om alles onder de cursor weer te geven; klik opnieuw op dezelfde plek om het volgende item daar te selecteren. Dubbelklik met de middelste knop om naar Home te gaan (Voorkeuren › Navigatie).",
"Select bodies (a face picks its body)": "Lichamen selecteren (een vlak kiest zijn lichaam)",
"Select bodies or faces instead": "In plaats daarvan lichamen of vlakken selecteren",
@@ -332,6 +334,7 @@
"Show Agent Cursor": "Agentcursor tonen",
"Show in folder": "Tonen in map",
"Show the pointer an AI agent moves (#35)": "De aanwijzer tonen die een AI-agent verplaatst (#35)",
"Show tool at cursor": "Gereedschap bij de cursor tonen",
"Signed axes": "Assen met teken",
"Size": "Grootte",
"Sketch constraints": "Schetsbeperkingen",
@@ -344,9 +347,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft is gesloten voordat deze ontwerpen zijn opgeslagen. Hun laatste automatische opslag kan worden geopend.",
"Specialise": "Specialiseren",
"Speed": "Snelheid",
"Square": "Vierkant",
"Stand it up": "Rechtop zetten",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Rechtop zetten: een kwartslag om X, als één Verplaatsen-kenmerk dat u ongedaan kunt maken.",
"Stand this part up?": "Dit onderdeel rechtop zetten?",
"Start angle": "Beginhoek",
"Start bulge": "Beginbolling",
"Start continuity": "Begincontinuïteit",
"Step": "Stap",
@@ -411,6 +416,7 @@
"angle, height": "hoek, hoogte",
"appearance.dielectric_ior": "Diëlektrische IOR",
"appearance.metallic": "Metaalachtig",
"appearance.only_available": "Alleen beschikbare verschijningen",
"appearance.pbr_help": "Bekijk dit oppervlak in Render of een product-PNG. Geen breking of transmissie.",
"appearance.pbr_opaque": "Ondoorzichtige PBR",
"appearance.roughness": "Ruwheid",
@@ -529,6 +535,10 @@
"cmd.file_insert_step": "STEP invoegen",
"cmd.file_save_mesh": "Opslaan als mesh",
"cmd.file_slicers": "Geïnstalleerde slicers",
"cmd.frame_member": "Constructiedeel",
"cmd.frame_profile_insert": "Profiel invoegen",
"cmd.frame_profile_list": "Profielbibliotheek",
"cmd.frame_profile_properties": "Profieleigenschappen",
"cmd.inspect_accessibility": "Toegankelijkheidsanalyse",
"cmd.inspect_center_of_mass": "Zwaartepunt",
"cmd.inspect_clearance": "Speling",
@@ -568,6 +578,7 @@
"cmd.material_create": "Nieuw fysiek materiaal",
"cmd.material_delete": "Fysiek materiaal verwijderen",
"cmd.material_edit": "Fysiek materiaal bewerken",
"cmd.material_inspect": "Fysiek materiaal inspecteren",
"cmd.material_list": "Materialen weergeven",
"cmd.material_properties_directional": "Richtingsafhankelijke elasticiteitsmodulus",
"cmd.material_properties_validate": "Gegevens fysiek materiaal valideren",
@@ -856,19 +867,57 @@
"help.mit": "MIT-licentie",
"help.notice": "NOTICE",
"help.third_party": "Licenties van derden",
"home.back": "Terug naar {name}",
"home.crashes": "Crashrapporten",
"home.design_workspace": "Ontwerp",
"home.documentation": "Documentatie",
"home.export": "Exporteren",
"home.export_hint": "Kies een indeling voor het huidige ontwerp.",
"home.export_json_hint": "Bewerkbaar ontwerp en opdrachtgeschiedenis",
"home.export_mesh_hint": "Mesh om af te drukken of te delen",
"home.export_step_hint": "Nauwkeurige volumegeometrie",
"home.import": "Importeren…",
"home.import_hint": "STEP, IGES of een mesh in een nieuw ontwerp",
"home.in_this_browser": "in deze browser",
"home.missing": "ontbreekt",
"home.month_01": "jan.",
"home.month_02": "feb.",
"home.month_03": "mrt.",
"home.month_04": "apr.",
"home.month_05": "mei",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "aug.",
"home.month_09": "sep.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Nieuw ontwerp in inches",
"home.new_inches_hint": "Inches · opent in Ontwerp",
"home.new_mm": "Nieuw ontwerp",
"home.new_mm_hint": "Millimeters · opent in Ontwerp",
"home.open": "Openen…",
"home.opened_days_ago": "{count} dagen geleden geopend",
"home.opened_hours_ago": "{count} u geleden geopend",
"home.opened_just_now": "Zojuist geopend",
"home.opened_minutes_ago": "{count} min geleden geopend",
"home.open_hint": "Een .solidcraft-ontwerp ({shortcut})",
"home.open_now": "Momenteel geopend",
"home.opened_days_ago": "{count} dagen geleden",
"home.opened_hours_ago": "{count} u geleden",
"home.opened_just_now": "Zojuist",
"home.opened_minutes_ago": "{count} min geleden",
"home.opening": "openen…",
"home.properties": "Ontwerpeigenschappen",
"home.recent": "Recent",
"home.recent_empty": "Ontwerpen die u opent of opslaat, verschijnen hier.",
"home.recovery_count": "Herstelbare ontwerpen: {count}",
"home.recovery_empty": "Geen herstelbare ontwerpen.",
"home.recovery_hint": "SolidCraft is gesloten voordat deze ontwerpen werden opgeslagen.",
"home.report_issue": "Probleem melden",
"home.samples": "Voorbeelden",
"home.settings": "Instellingen",
"home.short_date": "{day} {month}",
"home.shortcuts": "Sneltoetsen",
"home.start_hint": "Maak een ontwerp, open een recent bestand of bekijk een voorbeeld.",
"home.whats_new": "Wat is er nieuw",
"home.yesterday": "Gisteren",
"install.button_tip": "Installeer SolidCraft als app: deze opent in een eigen venster en werkt offline",
"install.dock_1": "Kies Bestand › Toevoegen aan Dock.",
"install.dock_2": "Klik op Voeg toe.",
@@ -1021,7 +1070,7 @@
"search.try": "Typ een opdrachtnaam, een kenmerk in dit ontwerp of een parameter. Om een opdracht met waarden uit te voeren, typt u de id en een JSON-object: {example}",
"search.valid": "Geldig",
"settings.display_quality_note": "Fijnere niveaus tekenen gebogen vlakken en randen vloeiender en kosten meer tijd om te meshen. Alleen de viewport verandert, nooit het ontwerp of de exports.",
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave. Zolang dit aan staat, wordt de weergave continu opnieuw getekend.",
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave.",
"settings.measure_axis_distances": "X/Y/Z-afstanden tonen",
"settings.measure_axis_distances_help": "Toon de componentmaten langs de wereldassen naast de totale afstand terwijl Meten open is.",
"settings.msaa_applies_on_restart": "Anti-aliasing wordt toegepast bij de volgende start van SolidCraft",
@@ -1082,7 +1131,50 @@
"trace.stopped_by_contact": "Eindigt bij monster {sample}: {a} zou door {b} heen gaan",
"trace.stopped_by_loop": "Eindigt bij monster {sample}: de lus via {joint} kan niet verder sluiten",
"ui.1_degree_of_freedom": "1 vrijheidsgraad",
"ui.3d_mouse_calibrate": "Kalibreren…",
"ui.3d_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
"ui.3d_mouse_calibrate_title": "3D-muis kalibreren",
"ui.3d_mouse_calibrate_wait": "Beweeg de 3D-muis één keer, zodat SolidCraft hem ziet.",
"ui.3d_mouse_calibrated": "Gekalibreerd",
"ui.3d_mouse_calibration": "Kalibratie van 3D-muis",
"ui.3d_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
"ui.3d_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
"ui.3d_mouse_circles_help": "De cirkels tonen wat het apparaat na kalibratie stuurt. Het testmodel toont wat de weergave ermee doet, na omkeren, snelheid en asindeling.",
"ui.3d_mouse_dead_zone": "Dode zone",
"ui.3d_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
"ui.3d_mouse_denied": "SolidCraft ziet een 3D-muis maar mag hem niet lezen. Zie de installatienotities voor de 3D-muis voor de udev-regel.",
"ui.3d_mouse_denied_short": "Gevonden, maar niet leesbaar",
"ui.3d_mouse_driver_fallback": "Voorlopig via spacenavd gelezen: het apparaat zelf kon niet worden geopend.",
"ui.3d_mouse_forget": "Kalibratie vergeten",
"ui.3d_mouse_ghost_left_viewport": "Viewport links",
"ui.3d_mouse_ghost_left_window": "Venster links",
"ui.3d_mouse_ghost_position": "Positie",
"ui.3d_mouse_ghost_right_viewport": "Viewport rechts",
"ui.3d_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
"ui.3d_mouse_navigate": "Navigeren met de 3D-muis",
"ui.3d_mouse_none": "Geen 3D-muis gevonden",
"ui.3d_mouse_not_calibrated": "Niet gekalibreerd",
"ui.3d_mouse_offer": "Je 3D-muis kalibreren verhelpt verwisselde of omgekeerde assen en stelt snelheid en gevoel in.",
"ui.3d_mouse_offer_never": "Niet meer tonen",
"ui.3d_mouse_offer_swap": "Naar Hidraw wisselen (direct)",
"ui.3d_mouse_reading": "Uitlezen",
"ui.3d_mouse_reset_view": "Weergave resetten",
"ui.3d_mouse_restore": "3D-muis standaardwaarden herstellen",
"ui.3d_mouse_restore_ask": "De 3D-muisinstellingen terugzetten naar de standaardwaarden? Je kalibratie en knoppentoewijzing blijven staan.",
"ui.3d_mouse_speed": "Snelheid van 3D-muis",
"ui.3d_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
"ui.3d_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
"ui.3d_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
"ui.3d_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
"ui.3d_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
"ui.3d_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
"ui.3d_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
"ui.3d_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
"ui.3d_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
"ui.3d_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
"ui.3d_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
"ui.3d_mouse_waiting": "Wacht op beweging",
"ui.3d_mouse_wiggle": "Als je er een hebt aangesloten, beweeg hem even.",
"ui.3d_sketch": "3D-schets",
"ui.activate": "Activeren",
"ui.active_component": "Actief component",
@@ -1092,6 +1184,7 @@
"ui.agent_chat_add": "Bestanden bijvoegen",
"ui.agent_chat_add_file": "Een bestand kiezen…",
"ui.agent_chat_allow": "Toestaan",
"ui.agent_chat_allow_once": "Eenmalig toestaan",
"ui.agent_chat_allowed": "Toegestaan",
"ui.agent_chat_answered": "Antwoord: {option}",
"ui.agent_chat_approval": "De agent dit laten doen?",
@@ -1143,12 +1236,16 @@
"ui.agent_chat_files_left_out": "Maximaal {max} bestanden per bericht; weggelaten: {files}",
"ui.agent_chat_files_loading": "Wachten tot de bestanden zijn gelezen",
"ui.agent_chat_files_max": "Maximaal {max} bestanden per bericht",
"ui.agent_chat_flatpak_none_found": "Geen codeeragent gevonden. De Flatpak-sandbox ziet standaard geen agents die in je persoonlijke map zijn geïnstalleerd en heeft geen netwerktoegang. Voer dit commando uit in een terminal en start SolidCraft daarna opnieuw om Claude Code en de ACP-adapter toe te staan die via nvm of in ~/.local/bin zijn geïnstalleerd. Het geeft toegang tot de genoemde programma’s en aanmeldbestanden, plus netwerktoegang:",
"ui.agent_chat_import_failed": "{name}: importeren mislukt: {error}",
"ui.agent_chat_intro": "Chat met een codeeragent die u al gebruikt, of met een modeleindpunt naar keuze. De agent werkt aan het geopende ontwerp via de tools van SolidCraft: de wijzigingen verschijnen terwijl ze worden gemaakt en worden per bericht als één stap ongedaan gemaakt. Wat hij zonder te vragen mag doen, stelt u in via Voorkeuren > AI.",
"ui.agent_chat_key_in_env": "sleutel ingesteld",
"ui.agent_chat_left": "Links",
"ui.agent_chat_limit": "De agent heeft de limiet van {limit} wijzigingen voor één bericht bereikt. Stuur nog een bericht om hem verder te laten gaan.",
"ui.agent_chat_minimize": "Minimaliseren tot een strook",
"ui.agent_chat_mode_all": "Alles",
"ui.agent_chat_mode_ask": "Vragen",
"ui.agent_chat_mode_edits": "Bewerken",
"ui.agent_chat_model_tip": "Het model dat de agent gebruikt. Uw keuze wordt voor deze agent onthouden.",
"ui.agent_chat_new": "Nieuwe chat",
"ui.agent_chat_new_confirm": "Een nieuwe chat starten? De huidige wordt gewist.",
@@ -1161,6 +1258,7 @@
"ui.agent_chat_none_found_short": "Geen agent gevonden",
"ui.agent_chat_not_installed": "Niet geïnstalleerd",
"ui.agent_chat_not_signed_in": "niet aangemeld",
"ui.agent_chat_open_settings": "Voorkeuren openen",
"ui.agent_chat_own_window": "Eigen venster",
"ui.agent_chat_paste_picture": "Een afbeelding plakken",
"ui.agent_chat_permission": "De agent vraagt om uit te voeren: {title}",
@@ -1185,6 +1283,8 @@
"ui.agent_chat_send": "Verzenden",
"ui.agent_chat_send_now": "Nu verzenden",
"ui.agent_chat_send_now_tip": "De huidige stap van de agent stoppen en dit nu verzenden (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft communiceert met een programmeeragent die op deze computer is geïnstalleerd. Kies er een via Voorkeuren › AI.",
"ui.agent_chat_setup_title": "Een assistent instellen",
"ui.agent_chat_side": "Zijde van agentchat",
"ui.agent_chat_signed_in": "aangemeld",
"ui.agent_chat_state_auth": "Aanmelden vereist",
@@ -1201,6 +1301,7 @@
"ui.agent_chat_undo_label": "Agentchat: {message}",
"ui.agent_chat_undone": "(ongedaan gemaakt)",
"ui.agent_chat_view_attached": "Weergave bijgevoegd",
"ui.agent_chat_waiting": "Wacht op jou",
"ui.agent_cursor": "Agentcursor",
"ui.agent_cursor_speed": "Snelheid agentcursor",
"ui.ai_add": "Toevoegen",
@@ -1258,7 +1359,6 @@
"ui.ai_permissions": "Machtigingen",
"ui.ai_preset": "Voorinstelling",
"ui.ai_remove": "Verwijderen",
"ui.ai_request_limit": "gestopt na 24 verzoeken voor één bericht; stuur nog een bericht om verder te gaan",
"ui.ai_reset": "Herstellen",
"ui.ai_settings": "AI-instellingen…",
"ui.ai_show_features": "AI-functies tonen",
@@ -1455,6 +1555,7 @@
"ui.grid_follows_view_hint": "Zijaanzichten tonen het raster op het rechtopstaande vlak dat naar hen toe gericht is in plaats van op de grond",
"ui.ground_shadow": "Grondschaduw",
"ui.groupSelected": "Selectie groeperen",
"ui.hatch": "Arcering",
"ui.heading_add_ins": "INVOEGMODULES",
"ui.heading_assemble": "SAMENSTELLEN",
"ui.heading_box": "KUB",
@@ -1528,6 +1629,11 @@
"ui.hideOrigin": "Oorsprong verbergen",
"ui.hidePlanes": "Vlakken verbergen",
"ui.hideProfile": "Profiel verbergen",
"ui.history_back": "Eén stap terug",
"ui.history_below_browser": "Onder de browser",
"ui.history_first": "Naar het begin",
"ui.history_forward": "Eén stap vooruit",
"ui.history_last": "Naar het einde",
"ui.history_title": "Geschiedenis",
"ui.hole": "Gat",
"ui.hole_tap_type": "Type tapgat",
@@ -1545,6 +1651,8 @@
"ui.joints": "Verbindingen",
"ui.keyboard_shortcuts": "Sneltoetsen",
"ui.language": "Taal",
"ui.layout_compact": "Compact",
"ui.layout_floating": "Zwevend",
"ui.length_decimals": "Lengtedecimalen",
"ui.light": "Licht",
"ui.line": "Lijn",
@@ -1682,6 +1790,7 @@
"ui.reverse_zoom_direction": "Zoomrichting omkeren",
"ui.revolve": "Revolve",
"ui.ribbon_exit": "Afsluiten",
"ui.ribbon_labels": "Tekst onder lintpictogrammen tonen",
"ui.ribbon_pin_footer": "Vastgepinde opdrachten verschijnen in het lint. Beweeg over een rij om vast te pinnen of los te maken.",
"ui.ribbon_workspace": "Werkruimte",
"ui.right": "Rechts",
@@ -1737,34 +1846,6 @@
"ui.smooth_wheel_zoom": "Vloeiend zoomen met wiel",
"ui.smooth_wheel_zoom_tip": "Uitschakelen om elke wielgebeurtenis direct toe te passen, zonder uitloop",
"ui.snap": "Snap",
"ui.space_mouse_calibrate": "Kalibreren…",
"ui.space_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
"ui.space_mouse_calibrate_title": "3D-muis kalibreren",
"ui.space_mouse_calibrated": "Gekalibreerd",
"ui.space_mouse_calibration": "Kalibratie van 3D-muis",
"ui.space_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
"ui.space_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
"ui.space_mouse_dead_zone": "Dode zone",
"ui.space_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
"ui.space_mouse_forget": "Kalibratie vergeten",
"ui.space_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
"ui.space_mouse_navigate": "Navigeren met SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Geen 3D-muis gevonden",
"ui.space_mouse_not_calibrated": "Niet gekalibreerd",
"ui.space_mouse_reading": "Uitlezen",
"ui.space_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
"ui.space_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
"ui.space_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
"ui.space_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
"ui.space_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
"ui.space_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
"ui.space_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
"ui.space_mouse_step_tilt_back": "Kantel de dop naar achteren (voorrand omhoog), houd vast en laat dan los.",
"ui.space_mouse_step_tilt_forward": "Kantel de dop naar voren (voorrand omlaag), houd vast en laat dan los.",
"ui.space_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
"ui.space_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
"ui.space_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
"ui.space_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
"ui.spacing": "Tussenruimte",
"ui.spacing_2": "Tussenruimte 2",
"ui.sphere": "Bol",
@@ -1836,6 +1917,7 @@
"ui.workspace_design_tabs": "Model · Oppervlak · Assembleren · Invoegen · Inspecteren · Beheren · Beeld",
"ui.workspace_drawing_description": "2D-tekeningen met weergaven, maatvoering en titelblokken",
"ui.workspace_footer": "Elke werkruimte werkt op hetzelfde ontwerp, dezelfde geschiedenis en selectie; alleen de lintabbladen veranderen. De schakelaar is vergrendeld terwijl een schets of kenmerk wordt bewerkt.",
"ui.workspace_layout": "Werkruimte-indeling",
"ui.workspace_manufacture_description": "Slicen, gereedschapspaden en opstelling",
"ui.workspace_planned": "Gepland",
"ui.workspace_render_description": "Productvoorbeeld, sceneinstellingen en gerenderde afbeeldingen",
+24
View File
@@ -0,0 +1,24 @@
# Pending UI strings
A pull request that adds UI strings adds **one new file here** instead of editing
`assets/locales/<locale>.json`. Two pull requests then never touch the same file, so they never
conflict.
- Name it after the change, lowercase with dashes: `sketch-trim-hints.json`.
- Contents: a map from locale code to `{"message id": "text"}`. Give English; add other locales
you can translate (a locale you leave out falls back to English, as before):
```json
{
"en": { "sketch.trim.hint": "Click a segment to trim it" },
"de": { "sketch.trim.hint": "Segment zum Kürzen anklicken" }
}
```
- The id must not already be in that locale's catalog, nor in another pending file; the build and
`cargo xtask lists` fail otherwise. To change an existing message, edit the catalog itself.
- Don't sort or fold anything. The app, `cargo xtask i18n` and the tests read these files as part of
the catalogs, so your strings work in your own pull request.
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it adds the
messages to the catalogs (sorted, one per line) and deletes the files. Never run it in a pull
request.
+369 -36
View File
@@ -28,6 +28,7 @@
"Also deletes": "Usuwa również",
"Angle 2": "Kąt 2",
"Animate": "Animuj",
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
"Appearance name": "Nazwa wyglądu",
"Applied Radius": "Zastosowany promień",
"Apply To": "Zastosuj do",
@@ -72,6 +73,7 @@
"Choose…": "Wybierz…",
"Circular": "Kołowy",
"Circumference": "Obwód",
"Clockwise": "Zgodnie z ruchem wskazówek zegara",
"Close draft": "Zamknij szkic zgłoszenia",
"Collisions": "Kolizje",
"Colour": "Kolor",
@@ -83,6 +85,7 @@
"Contact": "Kontakt",
"Contact Sets": "Zestawy kontaktów",
"Contact sets": "Zestawy kontaktów",
"Continue on GitHub": "Kontynuuj na GitHubie",
"Continuity": "Ciągłość",
"Copy Error Message": "Kopiuj komunikat błędu",
"Copy Warning Message": "Kopiuj komunikat ostrzeżenia",
@@ -102,6 +105,7 @@
"Curv. Comb": "Grzebień krzyw.",
"Curvature (G2)": "Krzywizna (G2)",
"Curvature change (G3)": "Zmiana krzywizny (G3)",
"Custom": "Niestandardowy",
"Customize Toolbar": "Dostosuj pasek narzędzi",
"Delete": "Usuń",
"Delete all": "Usuń wszystko",
@@ -110,6 +114,8 @@
"Delete the set": "Usuń zestaw",
"Density": "Gęstość",
"Dependents": "Elementy zależne",
"Describe the problem you saw": "Opisz zauważony problem",
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
"Direction": "Kierunek",
@@ -127,6 +133,7 @@
"Enabled": "Włączone",
"End bulge": "Wybrzuszenie końca",
"End continuity": "Ciągłość końca",
"Expected result (optional)": "Oczekiwany wynik (opcjonalnie)",
"Explode": "Rozstrzel",
"Explode Text": "Rozbij tekst",
"Export Animation…": "Eksportuj animację…",
@@ -167,6 +174,8 @@
"HISTORY": "HISTORIA",
"Height": "Wysokość",
"Hidden edges": "Krawędzie niewidoczne",
"History layout": "Układ historii",
"History names": "Nazwy w historii",
"Holes": "Otwory",
"Holes (edges on them)": "Otwory (krawędzie na nich)",
"Home view": "Widok główny",
@@ -232,7 +241,8 @@
"No crash reports.": "Brak raportów o awariach.",
"No matching appearance presets.": "Brak pasujących ustawień wyglądu.",
"No matching appearances": "Brak pasujących wyglądów",
"No visible bodies to export.": "Brak widocznych brył do wyeksportowania.",
"No visible bodies or sketches to export.": "Brak widocznych brył lub szkiców do wyeksportowania.",
"Nominal diameter": "Średnica nominalna",
"None": "Brak",
"Not available yet": "Jeszcze niedostępne",
"Notch": "Wycięcie",
@@ -241,7 +251,10 @@
"Open GitHub draft": "Otwórz szkic zgłoszenia na GitHubie",
"Open local libraries in the desktop app. You can export this design's library here.": "Otwórz biblioteki lokalne w aplikacji komputerowej. Tutaj możesz wyeksportować bibliotekę tego projektu.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Otwartoźródłowy parametryczny CAD 3D w języku Rust. Licencja MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Otwiera przeglądarkę internetową i kopiuje treść raportu, którą wkleisz na GitHubie.",
"Opens your browser. Review and submit your report on GitHub.": "Otwiera przeglądarkę internetową. Sprawdź raport i wyślij go na GitHubie.",
"Operator": "Operator",
"Orbit (right drag)": "Obracanie widoku (przeciąganie prawym przyciskiem)",
"Other side": "Druga strona",
"Other surface (mutual trim)": "Druga powierzchnia (wzajemne przycięcie)",
"Outside": "Na zewnątrz",
@@ -256,6 +269,7 @@
"Pick the tool bodies": "Wybierz bryły narzędziowe",
"Picked": "Wybrane",
"Picked point": "Wybrany punkt",
"Pitch": "Skok",
"Place at the origin instead": "Zamiast tego umieść w początku układu",
"Plastic Rules": "Reguły tworzyw",
"Playback": "Odtwarzanie",
@@ -289,6 +303,8 @@
"Remove the track": "Usuń ścieżkę",
"Remove this point": "Usuń ten punkt",
"Report a bug — review a screenshot and open a GitHub draft": "Zgłoś błąd — sprawdź zrzut ekranu i otwórz szkic zgłoszenia na GitHubie",
"Report text copied. Paste it into GitHub and submit your report there.": "Skopiowano treść raportu. Wklej ją na GitHubie i tam wyślij raport.",
"Reports are public. A GitHub account is required.": "Raporty są publiczne. Wymagane jest konto GitHub.",
"Reset Toolbar": "Przywróć pasek narzędzi",
"Resize Part": "Zmień rozmiar części",
"Restore View": "Przywróć widok",
@@ -303,6 +319,7 @@
"Revert Pos.": "Przywróć poł.",
"Revert Position": "Przywróć położenie",
"Review the report and screenshot before opening GitHub.": "Przed otwarciem GitHuba sprawdź raport i zrzut ekranu.",
"Review your report on GitHub and click Submit new issue to publish it.": "Sprawdź raport na GitHubie i kliknij Submit new issue, aby go opublikować.",
"Rim Edge": "Krawędź obrzeża",
"Roll History Marker Here": "Przesuń tutaj znacznik historii",
"Round": "Okrągłe",
@@ -313,14 +330,17 @@
"SURFACE QUALITY": "JAKOŚĆ POWIERZCHNI",
"Save Rule": "Zapisz regułę",
"Save View": "Zapisz widok",
"Save screenshot…": "Zapisz zrzut ekranu…",
"Save the study as an animated GIF, seen as the viewport shows it": "Zapisz badanie jako animowany GIF, tak jak widać je w widoku",
"Saved preview. Geometry is being checked before editing.": "Zapisany podgląd. Geometria jest sprawdzana przed edycją.",
"Scale factor": "Współczynnik skali",
"Scene": "Scena",
"Screenshot (optional)": "Zrzut ekranu (opcjonalnie)",
"Search": "Szukaj",
"Search commands": "Szukaj poleceń",
"Search this library": "Przeszukaj tę bibliotekę",
"Section": "Przekrój",
"Section position": "Położenie przekroju",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Wybierz inny: przytrzymaj nieruchomo lewy przycisk na modelu (lub prawy przycisk › Wybierz inny…), aby wyświetlić wszystko pod kursorem; kliknij ponownie w tym samym miejscu, aby zaznaczyć kolejny obiekt. Dwukrotne kliknięcie środkowym przyciskiem przechodzi do widoku głównego (Preferencje › Nawigacja).",
"Select bodies (a face picks its body)": "Zaznacz bryły (ściana wybiera swoją bryłę)",
"Select bodies or faces instead": "Zamiast tego zaznacz bryły lub ściany",
@@ -332,6 +352,7 @@
"Show Agent Cursor": "Pokaż kursor agenta",
"Show in folder": "Pokaż w folderze",
"Show the pointer an AI agent moves (#35)": "Pokaż wskaźnik poruszany przez agenta AI (#35)",
"Show tool at cursor": "Pokaż narzędzie przy kursorze",
"Signed axes": "Osie ze znakiem",
"Size": "Rozmiar",
"Sketch constraints": "Wiązania szkicu",
@@ -344,24 +365,30 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft zamknął się, zanim te projekty zostały zapisane. Można otworzyć ich ostatni autozapis.",
"Specialise": "Uszczegółów",
"Speed": "Prędkość",
"Square": "Kwadrat",
"Stand it up": "Postaw",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postaw: obrót o ćwierć obrotu wokół osi X, jako jedna funkcja Przesuń, którą można cofnąć.",
"Stand this part up?": "Postawić tę część?",
"Start angle": "Kąt początkowy",
"Start bulge": "Wybrzuszenie początku",
"Start continuity": "Ciągłość początku",
"Step": "Krok",
"Steps": "Kroki",
"Steps to reproduce (optional)": "Kroki do odtworzenia (opcjonalnie)",
"Stitch the kept pieces": "Zszyj zachowane części",
"Stitch to both bodies": "Zszyj z obiema bryłami",
"Study": "Badanie",
"Style": "Styl",
"Summary": "Podsumowanie",
"Suppressed": "Wygaszone",
"Surface": "Powierzchnia",
"Swap Ends": "Zamień końce",
"TRIM": "PRZYCINANIE",
"Tangent (G1)": "Styczność (G1)",
"Tangent Chain": "Łańcuch styczny",
"Tap drill diameter": "Średnica wiertła pod gwint",
"Tear": "Rozdarcie",
"Technical details": "Szczegóły techniczne",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Przerywany szkic to zapisana płaszczyzna pomocnicza. Wybierz nową ścianę, aby naprawić zaczepienie.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Brak zapisanego wystąpienia autora. Przed ponownym powiązaniem wybierz prawidłowe położenie autora.",
"Then": "Wtedy",
@@ -373,6 +400,7 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Ten element podrzędny jest współdzielony. Usunięcie go usuwa go z każdej kopii elementu nadrzędnego.",
"This child is shared. Position changes apply to every copy of its parent.": "Ten element podrzędny jest współdzielony. Zmiany położenia dotyczą każdej kopii elementu nadrzędnego.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ta ściana nie zapewnia trwałego zaczepienia. Wybierz inną ścianę lub jawnie użyj stałej płaszczyzny.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Ten obraz nie jest jeszcze dołączony na GitHubie. Skopiuj go lub zapisz, a potem dołącz na GitHubie.",
"To Offset": "Do odsunięcia",
"To edges": "Do krawędzi",
"Tolerance": "Tolerancja",
@@ -398,6 +426,9 @@
"Wall": "Ścianka",
"Wall Location": "Położenie ścianki",
"Wall Thickness": "Grubość ścianki",
"What did you do just before the problem? A short list is enough": "Co robiłeś tuż przed wystąpieniem problemu? Wystarczy krótka lista",
"What did you expect to see?": "Czego się spodziewałeś?",
"What happened?": "Co się stało?",
"When": "Gdy",
"Width": "Szerokość",
"Will fail": "Nie powiedzie się",
@@ -408,9 +439,33 @@
"Z angle": "Kąt Z",
"Z distance": "Odległość Z",
"Zebra": "Zebra",
"ai.prefs.availability": "Sterowanie asystentem",
"ai.prefs.background_commands": "Uruchamiaj polecenia agenta w tle",
"ai.prefs.follow_camera": "Obracaj kamerę do szkiców",
"ai.prefs.follow_camera_off": "Automatyczne podążanie kamery jest wyłączone.",
"ai.prefs.follow_camera_on": "Kamera obraca się przodem do szkiców tworzonych przez agenta.",
"ai.prefs.intro": "Agenty, uprawnienia i sposób pokazywania ich pracy.",
"ai.prefs.make_default": "Używaj domyślnie",
"ai.prefs.no_agents": "Brak agentów. Dodaj agenta lub odśwież, aby wyszukać zainstalowane agenty.",
"ai.prefs.no_saved_approvals": "Brak zapisanych zgód.",
"ai.prefs.override_all_warning": "Ten agent może zapisywać, nadpisywać, eksportować i zastępować projekt bez pytania.",
"ai.prefs.permission_all": "Zezwalaj na wszystko",
"ai.prefs.permission_all_summary": "Dostępne narzędzia SolidCraft działają bez pytania.",
"ai.prefs.permission_ask": "Pytaj przed zmianami",
"ai.prefs.permission_ask_summary": "Zatwierdzaj każdą zmianę projektu i każdą operację na plikach.",
"ai.prefs.permission_edits": "Zezwalaj na edycję modelu",
"ai.prefs.permission_edits_summary": "Edycja modelu działa bez pytania; operacje na plikach i nieodwracalne nadal wymagają pytania.",
"ai.prefs.permission_scope": "Dotyczy tylko narzędzi SolidCraft. Powłoka agenta i edycja plików podlegają jego własnym uprawnieniom.",
"ai.prefs.pointer_preview_hidden": "Wskaźnik ukryty",
"ai.prefs.pointer_preview_speed": "Wskaźnik agenta · {speed}",
"ai.prefs.pointer_speed": "Szybkość ruchu",
"ai.prefs.privacy_note": "Rozmowę otrzymuje tylko wybrany przez Ciebie agent lub punkt końcowy. Klucze punktów końcowych są przechowywane w systemowym pęku kluczy.",
"ai.prefs.show_pointer": "Pokaż wskaźnik agenta",
"ai.prefs.turn_limit_hint": "Maksymalna liczba wywołań narzędzi zmieniających projekt, które asystent może wykonać dla jednej wiadomości.",
"angle, height": "kąt, wysokość",
"appearance.dielectric_ior": "Współczynnik załamania dielektryka (IOR)",
"appearance.metallic": "Metaliczność",
"appearance.only_available": "Tylko dostępne wyglądy",
"appearance.pbr_help": "Zobacz tę powierzchnię w renderowaniu lub w obrazie PNG produktu. Bez załamania i przepuszczania światła.",
"appearance.pbr_opaque": "Nieprzezroczysty PBR",
"appearance.roughness": "Chropowatość",
@@ -529,6 +584,10 @@
"cmd.file_insert_step": "Wstaw STEP",
"cmd.file_save_mesh": "Zapisz jako siatkę",
"cmd.file_slicers": "Zainstalowane slicery",
"cmd.frame_member": "Element ramy",
"cmd.frame_profile_insert": "Wstaw profil",
"cmd.frame_profile_list": "Biblioteka profili",
"cmd.frame_profile_properties": "Właściwości profilu",
"cmd.inspect_accessibility": "Analiza dostępności",
"cmd.inspect_center_of_mass": "Środek masy",
"cmd.inspect_clearance": "Prześwit",
@@ -568,6 +627,7 @@
"cmd.material_create": "Nowy materiał fizyczny",
"cmd.material_delete": "Usuń materiał fizyczny",
"cmd.material_edit": "Edytuj materiał fizyczny",
"cmd.material_inspect": "Sprawdź materiał fizyczny",
"cmd.material_list": "Lista materiałów",
"cmd.material_properties_directional": "Kierunkowy moduł sprężystości",
"cmd.material_properties_validate": "Sprawdź dane materiału fizycznego",
@@ -758,6 +818,7 @@
"cmd.view_graphics_preset": "Ustawienie graficzne",
"cmd.view_list": "Lista widoków nazwanych",
"cmd.view_navigate": "Nawiguj widokiem",
"cmd.view_normal_to": "Prostopadle do zaznaczenia",
"cmd.view_rename": "Zmień nazwę widoku nazwanego",
"cmd.view_save": "Nowy widok nazwany",
"color.copy": "Kliknij, aby skopiować wartości koloru",
@@ -831,7 +892,7 @@
"dxf.progress.projection": "Rzutowanie geometrii (3/5)",
"dxf.progress.write": "Zapisywanie DXF (5/5)",
"dxf.tolerance": "Tolerancja krzywych",
"dxf.tolerance_help": "Tylko widoczne bryły. Krzywe są przybliżane odcinkami. Mniejsza tolerancja daje dokładniejsze krzywe, ale zajmuje więcej czasu i pamięci.",
"dxf.tolerance_help": "Tylko widoczne bryły i pokazane szkice. Krzywe są przybliżane odcinkami. Mniejsza tolerancja daje dokładniejsze krzywe, ale zajmuje więcej czasu i pamięci.",
"file_load.built": "Dotąd zbudowano: {built} z {total} elementów.",
"file_load.converting": "Konwertowanie bryły {done} z {total}",
"file_load.inserting": "Wstawianie geometrii",
@@ -855,20 +916,64 @@
"help.licences": "Licencje",
"help.mit": "Licencja MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Ścina zaznaczone krawędzie płaską fazą o zadanej odległości. Służy do łamania ostrych krawędzi lub ułatwienia wprowadzenia kołka czy śruby.",
"help.solid.extrude": "Wyciąga profil szkicu w 3D na zadaną odległość, dodając lub usuwając materiał. Służy do zamiany płaskiego obrysu w blok, występ lub kieszeń.",
"help.solid.fillet": "Zaokrągla zaznaczone krawędzie gładkim łukiem o zadanym promieniu. Służy do złagodzenia ostrych krawędzi dla wytrzymałości, wygody lub wyglądu.",
"help.solid.hole": "Wierci okrągły otwór o zadanej średnicy w wybranym punkcie ściany. Służy do otworów przelotowych pod śruby, kołki i innych prostych okrągłych otworów.",
"help.solid.revolve": "Obraca profil szkicu wokół osi, tworząc okrągłą bryłę. Służy do wałków, gałek, misek i wszystkich części okrągłych względem osi.",
"help.solid.shell": "Wydrąża bryłę, pozostawiając ścianki o zadanej grubości, i może otworzyć wybrane ściany. Służy do pudełek, pokryw i innych części cienkościennych.",
"help.third_party": "Licencje stron trzecich",
"home.back": "Wróć do {name}",
"home.crashes": "Raporty awarii",
"home.design_workspace": "Projektowanie",
"home.documentation": "Dokumentacja",
"home.export": "Eksportuj",
"home.export_hint": "Wybierz format dla bieżącego projektu.",
"home.export_json_hint": "Edytowalny projekt i historia poleceń",
"home.export_mesh_hint": "Siatka do druku lub udostępniania",
"home.export_step_hint": "Precyzyjna geometria bryłowa",
"home.import": "Importuj…",
"home.import_hint": "STEP, IGES lub siatka do nowego projektu",
"home.in_this_browser": "w tej przeglądarce",
"home.missing": "brak",
"home.month_01": "sty",
"home.month_02": "lut",
"home.month_03": "mar",
"home.month_04": "kwi",
"home.month_05": "maj",
"home.month_06": "cze",
"home.month_07": "lip",
"home.month_08": "sie",
"home.month_09": "wrz",
"home.month_10": "paź",
"home.month_11": "lis",
"home.month_12": "gru",
"home.new_inches": "Nowy projekt w calach",
"home.new_inches_hint": "Cale · otwiera w obszarze Projektowanie",
"home.new_mm": "Nowy projekt",
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
"home.open": "Otwórz…",
"home.opened_days_ago": "Otwarto {count} dni temu",
"home.opened_hours_ago": "Otwarto {count} godz. temu",
"home.opened_just_now": "Otwarto przed chwilą",
"home.opened_minutes_ago": "Otwarto {count} min temu",
"home.open_hint": "Projekt .solidcraft ({shortcut})",
"home.open_now": "Otwórz teraz",
"home.opened_days_ago": "{count} dni temu",
"home.opened_hours_ago": "{count} godz. temu",
"home.opened_just_now": "Przed chwilą",
"home.opened_minutes_ago": "{count} min temu",
"home.opening": "otwieranie…",
"home.properties": "Właściwości projektu",
"home.recent": "Ostatnie",
"home.recent_empty": "Tutaj pojawią się projekty, które otworzysz lub zapiszesz.",
"home.recovery_count": "Projekty do odzyskania: {count}",
"home.recovery_empty": "Brak projektów do odzyskania.",
"home.recovery_hint": "SolidCraft zamknął się przed zapisaniem tych projektów.",
"home.report_issue": "Zgłoś problem",
"home.samples": "Przykłady",
"home.settings": "Ustawienia",
"home.short_date": "{day} {month}",
"home.shortcuts": "Skróty klawiszowe",
"home.start_hint": "Utwórz projekt, otwórz ostatni plik lub obejrzyj przykład.",
"home.whats_new": "Co nowego",
"home.yesterday": "Wczoraj",
"install.button_tip": "Zainstaluj SolidCraft jako aplikację: otwiera się we własnym oknie i działa offline",
"install.dock_1": "Wybierz Plik › Dodaj do Docka.",
"install.dock_2": "Kliknij Dodaj.",
@@ -926,6 +1031,8 @@
"motion.to_end": "Na koniec",
"motion.to_start": "Na początek",
"motion.track_name": "Ścieżka {number}",
"nav.free_orbit.help": "Mysz i mysz 3D obracają widok przez oba bieguny i zachowują przechylenie. Przyciąganie do osi z Shift jest wyłączone.",
"nav.free_orbit.label": "Swobodne obracanie",
"nav.touchpad.help": "Przytrzymaj Shift i poruszaj wskaźnikiem, aby przesuwać; przytrzymaj Alt/Option i poruszaj nim, aby obracać widok. Przycisk myszy nie jest potrzebny. Przewijanie w pionie powiększa.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Obracanie widoku (przytrzymaj Alt/Option i poruszaj; bez przycisku myszy)",
@@ -948,8 +1055,130 @@
"pattern.flip_direction": "Odwróć kierunek",
"pick a hole to seat it, or OK places it at the origin": "wybierz otwór, aby go osadzić, lub OK umieszcza go w początku układu",
"pick one or two vertices, edges or faces": "wybierz jeden lub dwa wierzchołki, krawędzie lub ściany",
"prefs.add_item": "Dodaj element",
"prefs.alt_frees_manipulators": "Alt / Option zwalnia również strzałki manipulatora",
"prefs.anti_aliasing_note": "Wygładza poszarpane krawędzie.",
"prefs.applies_on_restart": "Działa po ponownym uruchomieniu",
"prefs.assistant": "Asystent",
"prefs.axes": "Osie",
"prefs.axis": "Oś",
"prefs.bottom": "Dół",
"prefs.browser": "Przeglądarka",
"prefs.built_in": "Wbudowana",
"prefs.button_left": "Przeciąganie lewym",
"prefs.button_middle": "Przeciąganie środkowym",
"prefs.button_right": "Przeciąganie prawym",
"prefs.choose_font": "Wybierz plik czcionki…",
"prefs.click": "Kliknięcie",
"prefs.contact_shading": "Miękkie cieniowanie kontaktowe",
"prefs.contact_shading_note": "Miękkie cieniowanie tam, gdzie części są blisko siebie.",
"prefs.context_menu": "Menu kontekstowe",
"prefs.context_menu_ring": "Menu kontekstowe · przytrzymaj, aby otworzyć menu promieniste",
"prefs.cpu": "CPU (programowo)",
"prefs.crash_reports": "Otwórz raporty o awariach…",
"prefs.crash_reports_note": "Raporty są przechowywane na tym komputerze, razem z projektem i poleceniami, które odtwarzają problem.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Powierzchnie zakrzywione",
"prefs.curves": "Powierzchnie zakrzywione",
"prefs.dead_zone_note": "Zakreskowany pas to strefa martwa: ruch wewnątrz niej jest pomijany.",
"prefs.default": "domyślnie",
"prefs.design": "Ustawienia domyślne projektu",
"prefs.design_note": "Ustawienia, z którymi startują nowe projekty. Otwarte projekty zachowują własne.",
"prefs.devices": "Mysz 3D",
"prefs.display": "Wyświetlanie",
"prefs.edges": "Krawędzie i cieniowanie",
"prefs.extend": "Rozszerz",
"prefs.extra_navigation": "Ctrl+przeciąganie lewym na pustym miejscu obraca widok; przytrzymaj Spację, aby przesuwać.",
"prefs.follows_os": "Zgodnie z systemem",
"prefs.font": "Czcionka",
"prefs.font_fallback": "Znaki, których brakuje w wybranej czcionce, są brane z czcionki wbudowanej, więc żadna etykieta nie pokaże pustego prostokąta.",
"prefs.font_sample": "Zażółć gęślą jaźń · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Nie można odczytać wybranej czcionki.",
"prefs.general": "Ogólne",
"prefs.general_note": "Język, zapisywanie i aktualizacje.",
"prefs.gesture_orbit": "Obracanie widoku",
"prefs.gesture_pan": "Przesuwanie",
"prefs.gesture_zoom": "Powiększanie",
"prefs.graphics": "Grafika",
"prefs.graphics_note": "Wybierz między szybkością a gładkością. Każda opcja pokazuje swój efekt na przykładzie.",
"prefs.graphics_presets_note": "Ustawienia wstępne zmieniają razem poniższe opcje.",
"prefs.history": "Historia",
"prefs.hold_stop_snapping": "Przytrzymaj, aby wyłączyć przyciąganie",
"prefs.icons_only": "Tylko ikony",
"prefs.imported_file": "Zaimportowany plik",
"prefs.input": "Wprowadzanie",
"prefs.interface": "Interfejs",
"prefs.interface_language": "Język interfejsu",
"prefs.interface_note": "Wygląd SolidCraft i rozmieszczenie jego elementów.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Etykiety",
"prefs.layout": "Układ",
"prefs.layout_note": "Kompaktowy dokuje panele do krawędzi okna. Przezroczystość przeglądarki jest dostępna w układzie Pływający. Stronę asystenta zmienisz na stronie Asystent.",
"prefs.left": "Lewo",
"prefs.left_click": "Kliknięcie lewym",
"prefs.live_input": "Bieżące wejście",
"prefs.live_preview": "Podgląd na żywo",
"prefs.minutes": "minut",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mysz i gładzik",
"prefs.mouse_heading": "Mysz",
"prefs.mouse_mapping": "Przypisanie przycisków myszy",
"prefs.navigation": "Nawigacja",
"prefs.navigation_note": "Jak mysz, gładzik i mysz 3D poruszają widokiem.",
"prefs.not_on_gpu": "niedostępne na tym GPU",
"prefs.on": "Włączone",
"prefs.opaque": "Nieprzezroczysta",
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
"prefs.orientation": "Orientacja",
"prefs.overall_speed": "Szybkość ogólna",
"prefs.preview_choices": "Podgląd wybranych opcji",
"prefs.quality_default": "domyślnie",
"prefs.quality_fastest": "najszybciej",
"prefs.quality_slowest": "najwolniej",
"prefs.quality_smoother": "gładziej",
"prefs.reports_saved": "Zapisane: {n}",
"prefs.right_click": "Kliknięcie prawym",
"prefs.saving": "Autozapis i odzyskiwanie",
"prefs.scale_note": "Także Ctrl + / Ctrl − w oknie. Przyciski mają 28 px przy 100%.",
"prefs.scale_text": "Skala i tekst",
"prefs.select": "Zaznacz",
"prefs.select_one_item": "Zaznacz jeden element",
"prefs.selecting": "Zaznaczanie",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift tylko dodaje; Ctrl / Cmd przełącza",
"prefs.shift_add_note": "Włączone: Shift dodaje, Ctrl / Cmd przełącza. Wyłączone (domyślnie): oba przełączają.",
"prefs.shortcuts": "Skróty",
"prefs.sketch_selection": "Szkic i zaznaczanie",
"prefs.sketch_selection_heading": "Szkic i zaznaczanie",
"prefs.sketch_selection_note": "Jak działa przyciąganie w szkicach i jak kliknięcia zaznaczają.",
"prefs.sketching": "Szkicowanie",
"prefs.snap_drawing_note": "Podczas rysowania lub przeciągania punktu szkicu.",
"prefs.source": "Źródło",
"prefs.speed": "Szybkość",
"prefs.stand_up_note": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X.",
"prefs.status_done": "Gotowe",
"prefs.status_error": "Błąd",
"prefs.status_selected": "Zaznaczone",
"prefs.status_warning": "Ostrzeżenie",
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
"prefs.theme_file": "plik motywu",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "teraz",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Dodaj element lub usuń go ponownie",
"prefs.toolbar": "Pasek narzędzi",
"prefs.translucent": "Półprzezroczysta",
"prefs.under_browser": "Pod przeglądarką",
"prefs.under_cursor": "Pod kursorem",
"prefs.units_precision": "Jednostki i dokładność",
"prefs.updates": "Aktualizacje",
"prefs.view": "Zachowanie widoku",
"prefs.viewport": "Widok",
"prefs.wheel": "Kółko",
"prefs.wheel_trackpad": "Kółko i gładzik",
"prefs.workspace": "Środowisko pracy",
"prefs.zoom_toward_cursor": "Powiększanie w stronę kursora",
"properties.title": "Właściwości: {name}",
"recovery.days_ago": "{count} dni temu",
"recovery.detail": "{file} · elementy: {features} · autozapis {when}",
@@ -1021,12 +1250,14 @@
"search.try": "Wpisz nazwę polecenia, element tego projektu lub parametr. Aby uruchomić polecenie z wartościami, wpisz jego identyfikator i obiekt JSON: {example}",
"search.valid": "Prawidłowe",
"settings.display_quality_note": "Wyższe poziomy rysują zakrzywione ściany i krawędzie gładziej, ale siatka powstaje dłużej. Zmienia się tylko widok, nigdy projekt ani eksport.",
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku. Gdy jest włączone, widok odświeża się bez przerwy.",
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku.",
"settings.measure_axis_distances": "Pokaż odległości X/Y/Z",
"settings.measure_axis_distances_help": "Gdy pomiar jest otwarty, pokazuj składowe wzdłuż osi globalnych obok odległości całkowitej.",
"settings.msaa_applies_on_restart": "Wygładzanie krawędzi zadziała po ponownym uruchomieniu SolidCraft",
"settings.msaa_reset": "{setting}: {requested}× nie jest obsługiwane przez to urządzenie graficzne; ustawiono {reset}× (obsługiwane: {supported}).",
"settings.msaa_unsupported": "Nieobsługiwane przez to urządzenie graficzne",
"settings.reference_geometry_through_bodies": "Pokaż geometrię odniesienia przez bryły",
"settings.reference_geometry_through_bodies_help": "Rysuj kontury nieaktywnych szkiców i płaszczyzny odniesienia na cieniowanych bryłach",
"settings.reset_intro": "Te ustawienia nie działają na tym komputerze i uniemożliwiłyby uruchomienie SolidCraft, więc zostały zmienione:",
"settings.reset_title": "Zmieniono ustawienia",
"settings.supersampling_note": "Rysuje widok w wyższej rozdzielczości i go zmniejsza: gładsze krawędzie, linie i siatka na ekranach poniżej 4K. Kosztuje czas GPU: ok. 2,25× więcej pikseli przy 1,5× i 4× przy 2×.",
@@ -1066,6 +1297,9 @@
"thread.designation": "Oznaczenie",
"thread.iso_metric": "Metryczny ISO",
"thread.select_size": "Wybierz rozmiar",
"timeline.delete_after_marker_one": "Usuń 1 funkcję za znacznikiem historii",
"timeline.delete_after_marker_other": "Usuń funkcje za znacznikiem historii: {count}",
"timeline.delete_many": "Usuń zaznaczone funkcje: {count}",
"timeline.group": "Nowy folder funkcji",
"timeline.move_many": "Przenieś elementy osi czasu",
"timeline.move_to_group": "Przenieś funkcje do folderu",
@@ -1082,7 +1316,106 @@
"trace.stopped_by_contact": "Kończy się na próbce {sample}: {a} przeszedłby przez {b}",
"trace.stopped_by_loop": "Kończy się na próbce {sample}: pętla przez {joint} nie może się bardziej zamknąć",
"ui.1_degree_of_freedom": "1 stopień swobody",
"ui.3d_mouse_axes_reassigned": "Osie są przypisane na nowo w Ustawieniach i to nadal obowiązuje razem z tą kalibracją.",
"ui.3d_mouse_button": "Przycisk {number}",
"ui.3d_mouse_button_nothing": "Nic",
"ui.3d_mouse_button_settings": "Ustawienia myszy 3D",
"ui.3d_mouse_buttons": "Przyciski",
"ui.3d_mouse_buttons_hint": "Naciśnij przycisk na myszy 3D, aby go tu znaleźć, a potem wybierz, co ma robić.",
"ui.3d_mouse_buttons_no_device": "Podłącz mysz 3D, aby ustawić, co robią jej przyciski.",
"ui.3d_mouse_buttons_reset": "Przywróć domyślne przyciski",
"ui.3d_mouse_calibrate": "Kalibruj…",
"ui.3d_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
"ui.3d_mouse_calibrate_title": "Kalibracja myszy 3D",
"ui.3d_mouse_calibrate_wait": "Rusz raz myszą 3D, aby SolidCraft ją zobaczył.",
"ui.3d_mouse_calibrated": "Skalibrowano",
"ui.3d_mouse_calibration": "Kalibracja myszy 3D",
"ui.3d_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
"ui.3d_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
"ui.3d_mouse_circles_help": "Koła pokazują, co urządzenie wysyła po kalibracji. Model testowy pokazuje, co widok z tym robi po odwróceniu, szybkości i przypisaniu osi.",
"ui.3d_mouse_dead_zone": "Strefa martwa",
"ui.3d_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
"ui.3d_mouse_denied": "SolidCraft widzi mysz 3D, ale nie ma pozwolenia, by ją odczytać. Regułę udev opisują uwagi o konfiguracji myszy 3D.",
"ui.3d_mouse_denied_short": "Znaleziono, ale nie można odczytać",
"ui.3d_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
"ui.3d_mouse_driven_by_label": "sterowana przez",
"ui.3d_mouse_driver": "Sterownik myszy 3D",
"ui.3d_mouse_driver_auto": "Automatyczny",
"ui.3d_mouse_driver_direct": "Bezpośredni (hidraw)",
"ui.3d_mouse_driver_fallback": "Na razie odczyt przez spacenavd: samego urządzenia nie udało się otworzyć.",
"ui.3d_mouse_driver_only_direct": "Urządzenie jest czytane bezpośrednio; spacenavd nie działa",
"ui.3d_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyna dostępna droga",
"ui.3d_mouse_forget": "Zapomnij kalibrację",
"ui.3d_mouse_ghost_left_viewport": "Lewy widok",
"ui.3d_mouse_ghost_left_window": "Lewe okno",
"ui.3d_mouse_ghost_position": "Położenie",
"ui.3d_mouse_ghost_right_viewport": "Prawy widok",
"ui.3d_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
"ui.3d_mouse_kind_cap": "Powrót do środka",
"ui.3d_mouse_kind_question": "Jaki to rodzaj myszy 3D?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "Odczyt zostaje, dopóki trzymasz, jak w urządzeniu typu „Powrót do środka”.",
"ui.3d_mouse_looks_like_trackball": "Odczyt wraca, gdy tylko przestajesz ruszać, jak w trackballu.",
"ui.3d_mouse_motion_rotation_x": "Obrót X",
"ui.3d_mouse_motion_rotation_y": "Obrót Y",
"ui.3d_mouse_motion_rotation_z": "Obrót Z",
"ui.3d_mouse_motion_translation_x": "Przesunięcie X",
"ui.3d_mouse_motion_translation_y": "Przesunięcie Y",
"ui.3d_mouse_motion_translation_z": "Przesunięcie Z",
"ui.3d_mouse_navigate": "Nawiguj za pomocą myszy 3D",
"ui.3d_mouse_none": "Nie znaleziono myszy 3D",
"ui.3d_mouse_not_calibrated": "Nieskalibrowano",
"ui.3d_mouse_offer": "Kalibracja myszy 3D naprawia zamienione lub odwrócone osie oraz ustawia jej szybkość i odczucie.",
"ui.3d_mouse_offer_never": "Nie pokazuj więcej",
"ui.3d_mouse_offer_swap": "Przełącz na Hidraw (sterownik bezpośredni)",
"ui.3d_mouse_reading": "Odczyt",
"ui.3d_mouse_reset_axes": "Przywróć przypisanie osi",
"ui.3d_mouse_reset_view": "Resetuj widok",
"ui.3d_mouse_restore": "Przywróć ustawienia myszy 3D",
"ui.3d_mouse_restore_ask": "Przywrócić domyślne ustawienia myszy 3D? Kalibracja i przypisanie przycisków zostają.",
"ui.3d_mouse_section": "Mysz 3D",
"ui.3d_mouse_show_in_view": "Pokazuj mysz 3D w widoku 3D",
"ui.3d_mouse_show_in_view_tip": "Obraz twojej myszy 3D w lewym dolnym rogu, który porusza się razem z nią.",
"ui.3d_mouse_skip": "Moje urządzenie tego nie potrafi",
"ui.3d_mouse_skip_tip": "Pomija ten ruch w obu kierunkach; kalibracja go nie uwzględnia.",
"ui.3d_mouse_speed": "Czułość myszy 3D",
"ui.3d_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
"ui.3d_mouse_step_rest_trackball": "Zdejmij dłoń z kulki i nie ruszaj jej.",
"ui.3d_mouse_step_ring_clockwise": "Obróć pierścień przewijania o kilka kliknięć zgodnie z ruchem wskazówek zegara.",
"ui.3d_mouse_step_ring_counterclockwise": "Obróć pierścień przewijania o kilka kliknięć przeciwnie do ruchu wskazówek zegara.",
"ui.3d_mouse_step_roll_away": "Potocz kulkę kilka razy od siebie, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_roll_left": "Potocz kulkę kilka razy w lewo, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_roll_right": "Potocz kulkę kilka razy w prawo, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_roll_toward": "Potocz kulkę kilka razy do siebie, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_spin_clockwise": "Zakręć kulką kilka razy zgodnie z ruchem wskazówek zegara, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_spin_counterclockwise": "Zakręć kulką kilka razy przeciwnie do ruchu wskazówek zegara, a potem pozwól jej się zatrzymać.",
"ui.3d_mouse_step_tilt_away": "Przechyl górę nakładki od siebie (krawędź najbliżej ciebie idzie w górę), przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_tilt_toward": "Przechyl górę nakładki do siebie (krawędź najbliżej ciebie idzie w dół), przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_touch": "Połóż dłoń na nakładce, nie naciskając jej.",
"ui.3d_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.3d_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.3d_mouse_switch_cap": "Kalibruj jako powrót do środka",
"ui.3d_mouse_switch_trackball": "Kalibruj jako trackball",
"ui.3d_mouse_view_axis": "Oś widoku",
"ui.3d_mouse_view_off": "Wyłączona",
"ui.3d_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tu ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
"ui.3d_mouse_view_orbit_sideways": "Orbita w lewo / prawo",
"ui.3d_mouse_view_orbit_up_down": "Orbita w górę / dół",
"ui.3d_mouse_view_pan_sideways": "Przesuwanie w lewo / prawo",
"ui.3d_mouse_view_pan_up_down": "Przesuwanie w górę / dół",
"ui.3d_mouse_view_roll": "Przechył",
"ui.3d_mouse_view_unmapped": "Oś {n}",
"ui.3d_mouse_view_zoom": "Powiększenie",
"ui.3d_mouse_waiting": "Czekam na ruch",
"ui.3d_mouse_wiggle": "Jeśli masz podłączoną, poruszaj nią chwilę.",
"ui.3d_sketch": "Szkic 3D",
"ui.activate": "Aktywuj",
"ui.active_component": "Aktywny komponent",
@@ -1092,6 +1425,7 @@
"ui.agent_chat_add": "Załącz pliki",
"ui.agent_chat_add_file": "Wybierz plik…",
"ui.agent_chat_allow": "Zezwól",
"ui.agent_chat_allow_once": "Zezwól raz",
"ui.agent_chat_allowed": "Zezwolono",
"ui.agent_chat_answered": "Odpowiedź: {option}",
"ui.agent_chat_approval": "Zezwolić agentowi na to działanie?",
@@ -1143,12 +1477,16 @@
"ui.agent_chat_files_left_out": "Do jednej wiadomości można dołączyć najwyżej {max} plików; pominięto: {files}",
"ui.agent_chat_files_loading": "Oczekiwanie na odczytanie plików",
"ui.agent_chat_files_max": "Najwyżej {max} plików w jednej wiadomości",
"ui.agent_chat_flatpak_none_found": "Nie znaleziono agenta programistycznego. Piaskownica Flatpak domyślnie nie widzi agentów zainstalowanych w folderze domowym i nie ma dostępu do sieci. Aby zezwolić na Claude Code i jego adapter ACP zainstalowane przez nvm lub w ~/.local/bin, uruchom to polecenie w terminalu, a potem uruchom ponownie SolidCraft. Daje ono dostęp do wymienionych programów i plików logowania oraz dostęp do sieci:",
"ui.agent_chat_import_failed": "{name}: import się nie powiódł: {error}",
"ui.agent_chat_intro": "Rozmawiaj z agentem programistycznym, którego już używasz, lub z wybranym punktem końcowym modelu. Pracuje on na otwartym projekcie za pomocą narzędzi SolidCraft: jego zmiany są widoczne na bieżąco, a cofa się je jednym krokiem na wiadomość. To, co może robić bez pytania, ustawisz w Preferencje > AI.",
"ui.agent_chat_key_in_env": "klucz ustawiony",
"ui.agent_chat_left": "Lewa",
"ui.agent_chat_limit": "Agent osiągnął limit {limit} zmian na jedną wiadomość. Wyślij kolejną wiadomość, aby mógł kontynuować.",
"ui.agent_chat_minimize": "Zminimalizuj do paska",
"ui.agent_chat_mode_all": "Wszystko",
"ui.agent_chat_mode_ask": "Pytaj",
"ui.agent_chat_mode_edits": "Edycje",
"ui.agent_chat_model_tip": "Model używany przez agenta. Twój wybór jest zapamiętywany dla tego agenta.",
"ui.agent_chat_new": "Nowy czat",
"ui.agent_chat_new_confirm": "Rozpocząć nowy czat? Bieżący zostanie wyczyszczony.",
@@ -1161,6 +1499,7 @@
"ui.agent_chat_none_found_short": "Nie znaleziono agenta",
"ui.agent_chat_not_installed": "Nie zainstalowano",
"ui.agent_chat_not_signed_in": "nie zalogowano",
"ui.agent_chat_open_settings": "Otwórz preferencje",
"ui.agent_chat_own_window": "Osobne okno",
"ui.agent_chat_paste_picture": "Wklej obraz",
"ui.agent_chat_permission": "Agent prosi o uruchomienie: {title}",
@@ -1185,6 +1524,8 @@
"ui.agent_chat_send": "Wyślij",
"ui.agent_chat_send_now": "Wyślij teraz",
"ui.agent_chat_send_now_tip": "Zatrzymaj bieżący krok agenta i wyślij tę wiadomość jako następną (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft komunikuje się z agentem programistycznym zainstalowanym na tym komputerze. Wybierz go w Preferencje › AI.",
"ui.agent_chat_setup_title": "Skonfiguruj asystenta",
"ui.agent_chat_side": "Strona czatu z agentem",
"ui.agent_chat_signed_in": "zalogowano",
"ui.agent_chat_state_auth": "Wymagane logowanie",
@@ -1201,8 +1542,11 @@
"ui.agent_chat_undo_label": "Czat z agentem: {message}",
"ui.agent_chat_undone": "(cofnięto)",
"ui.agent_chat_view_attached": "Dołączono widok",
"ui.agent_chat_waiting": "Czeka na Ciebie",
"ui.agent_cursor": "Kursor agenta",
"ui.agent_cursor_speed": "Szybkość kursora agenta",
"ui.agent_off_thread": "Zachowaj responsywność okna, gdy agenty wykonują polecenia",
"ui.agent_off_thread_note": "Wolne polecenia agenta (z panelu AI lub klienta MCP) są liczone obok okna: możesz dalej obracać i oglądać widok oraz zatrzymać polecenie przyciskiem w widoku. Wyłączone: każde działa w wątku okna, a okno na nie czeka.",
"ui.ai_add": "Dodaj",
"ui.ai_add_agent": "Dodaj agenta…",
"ui.ai_add_agent_title": "Dodaj agenta",
@@ -1258,7 +1602,6 @@
"ui.ai_permissions": "Uprawnienia",
"ui.ai_preset": "Ustawienie wstępne",
"ui.ai_remove": "Usuń",
"ui.ai_request_limit": "zatrzymano po 24 żądaniach dla jednej wiadomości; wyślij kolejną wiadomość, aby kontynuować",
"ui.ai_reset": "Resetuj",
"ui.ai_settings": "Ustawienia AI…",
"ui.ai_show_features": "Pokaż funkcje AI",
@@ -1303,6 +1646,7 @@
"ui.blocked": "Zablokowane",
"ui.bodies": "Bryły",
"ui.bodies_all_if_none": "Bryły (wszystkie, jeśli brak)",
"ui.bottom": "Dół",
"ui.box": "Prostopadłościan",
"ui.break": "Podziel",
"ui.browser_bodies_one": "{count} bryła",
@@ -1455,6 +1799,7 @@
"ui.grid_follows_view_hint": "Widoki boczne pokazują siatkę na pionowej płaszczyźnie zwróconej do nich zamiast na podłożu",
"ui.ground_shadow": "Cień na podłożu",
"ui.groupSelected": "Grupuj zaznaczone",
"ui.hatch": "Kreskowanie",
"ui.heading_add_ins": "DODATKI",
"ui.heading_assemble": "ZŁOŻENIE",
"ui.heading_box": "PROSTOPADŁOŚCIAN",
@@ -1528,7 +1873,19 @@
"ui.hideOrigin": "Ukryj początek układu",
"ui.hidePlanes": "Ukryj płaszczyzny",
"ui.hideProfile": "Ukryj profil",
"ui.history_back": "Cofnij o jeden krok",
"ui.history_below_browser": "Pod przeglądarką",
"ui.history_edge_hint": "kliknij dwukrotnie krawędź",
"ui.history_first": "Przejdź do początku",
"ui.history_forward": "Przejdź o jeden krok do przodu",
"ui.history_hide_names": "Ukryj nazwy",
"ui.history_last": "Przejdź do końca",
"ui.history_marker_tip": "Znacznik historii: przeciągnij, aby cofnąć model; nowe funkcje trafiają tutaj",
"ui.history_names": "Nazwy",
"ui.history_play": "Odtwórz do końca",
"ui.history_show_names": "Pokaż nazwy",
"ui.history_title": "Historia",
"ui.history_toggle_names": "Pokaż lub ukryj nazwy funkcji",
"ui.hole": "Otwór",
"ui.hole_tap_type": "Typ gwintu otworu",
"ui.hole_type": "Typ otworu",
@@ -1545,6 +1902,8 @@
"ui.joints": "Złącza",
"ui.keyboard_shortcuts": "Skróty klawiszowe",
"ui.language": "Język",
"ui.layout_compact": "Kompaktowy",
"ui.layout_floating": "Pływający",
"ui.length_decimals": "Miejsca dziesiętne długości",
"ui.light": "Jasny",
"ui.line": "Linia",
@@ -1682,6 +2041,7 @@
"ui.reverse_zoom_direction": "Odwróć kierunek powiększania",
"ui.revolve": "Obrót",
"ui.ribbon_exit": "Wyjdź",
"ui.ribbon_labels": "Pokazuj tekst pod ikonami wstążki",
"ui.ribbon_pin_footer": "Przypięte polecenia są widoczne na wstążce. Najedź na wiersz, aby go przypiąć lub odpiąć.",
"ui.ribbon_workspace": "Przestrzeń robocza",
"ui.right": "Prawy",
@@ -1737,34 +2097,6 @@
"ui.smooth_wheel_zoom": "Płynne powiększanie kółkiem",
"ui.smooth_wheel_zoom_tip": "Wyłącz, aby każdy ruch kółka działał od razu, bez wygładzania",
"ui.snap": "Przyciąganie",
"ui.space_mouse_calibrate": "Kalibruj…",
"ui.space_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
"ui.space_mouse_calibrate_title": "Kalibracja myszy 3D",
"ui.space_mouse_calibrated": "Skalibrowano",
"ui.space_mouse_calibration": "Kalibracja myszy 3D",
"ui.space_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
"ui.space_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
"ui.space_mouse_dead_zone": "Strefa martwa",
"ui.space_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
"ui.space_mouse_forget": "Zapomnij kalibrację",
"ui.space_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
"ui.space_mouse_navigate": "Nawiguj za pomocą SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Nie znaleziono myszy 3D",
"ui.space_mouse_not_calibrated": "Nieskalibrowano",
"ui.space_mouse_reading": "Odczyt",
"ui.space_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
"ui.space_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_tilt_back": "Przechyl nakładkę do tyłu (przednia krawędź w górę), przytrzymaj, a następnie puść.",
"ui.space_mouse_step_tilt_forward": "Przechyl nakładkę do przodu (przednia krawędź w dół), przytrzymaj, a następnie puść.",
"ui.space_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
"ui.space_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
"ui.space_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.spacing": "Odstęp",
"ui.spacing_2": "Odstęp 2",
"ui.sphere": "Sfera",
@@ -1836,6 +2168,7 @@
"ui.workspace_design_tabs": "Model · Powierzchnia · Złożenie · Wstaw · Sprawdź · Zarządzaj · Widok",
"ui.workspace_drawing_description": "Rysunki 2D z widokami, wymiarami i tabliczkami rysunkowymi",
"ui.workspace_footer": "Każda przestrzeń robocza działa na tym samym projekcie, historii i zaznaczeniu; zmieniają się tylko karty wstążki. Przełącznik jest zablokowany podczas edycji szkicu lub elementu.",
"ui.workspace_layout": "Układ obszaru roboczego",
"ui.workspace_manufacture_description": "Cięcie na warstwy, ścieżki narzędzia i przygotowanie",
"ui.workspace_planned": "Planowane",
"ui.workspace_render_description": "Podgląd produktu, ustawienia sceny i renderowane obrazy",
+182 -35
View File
@@ -72,6 +72,7 @@
"Choose…": "Escolher…",
"Circular": "Circular",
"Circumference": "Circunferência",
"Clockwise": "Horário",
"Close draft": "Fechar rascunho",
"Collisions": "Colisões",
"Colour": "Cor",
@@ -167,6 +168,7 @@
"HISTORY": "HISTÓRICO",
"Height": "Altura",
"Hidden edges": "Arestas ocultas",
"History layout": "Layout do histórico",
"Holes": "Furos",
"Holes (edges on them)": "Furos (arestas neles)",
"Home view": "Vista inicial",
@@ -232,7 +234,7 @@
"No crash reports.": "Nenhum relatório de falha.",
"No matching appearance presets.": "Nenhuma predefinição de aparência correspondente.",
"No matching appearances": "Nenhuma aparência correspondente",
"No visible bodies to export.": "Nenhum corpo visível para exportar.",
"No visible bodies or sketches to export.": "Nenhum corpo ou esboço visível para exportar.",
"None": "Nenhum",
"Not available yet": "Ainda não disponível",
"Notch": "Entalhe",
@@ -321,6 +323,7 @@
"Search commands": "Pesquisar comandos",
"Search this library": "Pesquisar nesta biblioteca",
"Section": "Seção",
"Section position": "Posição da seção",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Selecionar outro: mantenha o botão esquerdo parado sobre o modelo (ou clique com o botão direito › Selecionar outro…) para listar tudo sob o cursor; clique de novo no mesmo ponto para selecionar o próximo item ali. Clique duas vezes com o botão do meio para ir ao Início (Preferências › Navegação).",
"Select bodies (a face picks its body)": "Selecione corpos (uma face seleciona seu corpo)",
"Select bodies or faces instead": "Selecionar corpos ou faces em vez disso",
@@ -332,6 +335,7 @@
"Show Agent Cursor": "Mostrar cursor do agente",
"Show in folder": "Mostrar na pasta",
"Show the pointer an AI agent moves (#35)": "Mostrar o ponteiro que um agente de IA move (#35)",
"Show tool at cursor": "Mostrar a ferramenta junto ao cursor",
"Signed axes": "Eixos com sinal",
"Size": "Tamanho",
"Sketch constraints": "Restrições do esboço",
@@ -344,9 +348,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "O SolidCraft fechou antes de estes projetos serem salvos. O último salvamento automático deles pode ser aberto.",
"Specialise": "Especializar",
"Speed": "Velocidade",
"Square": "Quadrado",
"Stand it up": "Colocar em pé",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Colocar em pé: um quarto de volta em torno de X, como um recurso Mover que pode ser desfeito.",
"Stand this part up?": "Colocar esta peça em pé?",
"Start angle": "Ângulo inicial",
"Start bulge": "Abaulamento inicial",
"Start continuity": "Continuidade inicial",
"Step": "Passo",
@@ -411,6 +417,7 @@
"angle, height": "ângulo, altura",
"appearance.dielectric_ior": "IOR dielétrico",
"appearance.metallic": "Metálico",
"appearance.only_available": "Somente aparências disponíveis",
"appearance.pbr_help": "Veja esta superfície em Renderizar ou em um PNG de produto. Sem refração nem transmissão.",
"appearance.pbr_opaque": "PBR opaco",
"appearance.roughness": "Rugosidade",
@@ -529,6 +536,10 @@
"cmd.file_insert_step": "Inserir STEP",
"cmd.file_save_mesh": "Salvar como malha",
"cmd.file_slicers": "Fatiadores instalados",
"cmd.frame_member": "Membro estrutural",
"cmd.frame_profile_insert": "Inserir perfil",
"cmd.frame_profile_list": "Biblioteca de perfis",
"cmd.frame_profile_properties": "Propriedades do perfil",
"cmd.inspect_accessibility": "Análise de acessibilidade",
"cmd.inspect_center_of_mass": "Centro de massa",
"cmd.inspect_clearance": "Folga",
@@ -568,6 +579,7 @@
"cmd.material_create": "Novo material físico",
"cmd.material_delete": "Excluir material físico",
"cmd.material_edit": "Editar material físico",
"cmd.material_inspect": "Inspecionar material físico",
"cmd.material_list": "Listar materiais",
"cmd.material_properties_directional": "Módulo de elasticidade direcional",
"cmd.material_properties_validate": "Validar dados do material físico",
@@ -754,12 +766,19 @@
"cmd.timeline_suppress": "Suprimir recurso",
"cmd.view_ambient_occlusion": "Oclusão de ambiente",
"cmd.view_apply": "Restaurar vista nomeada",
"cmd.view_back": "Vista traseira",
"cmd.view_bottom": "Vista inferior",
"cmd.view_delete": "Excluir vista nomeada",
"cmd.view_front": "Vista frontal",
"cmd.view_graphics_preset": "Predefinição gráfica",
"cmd.view_isometric": "Vista isométrica",
"cmd.view_left": "Vista esquerda",
"cmd.view_list": "Listar vistas nomeadas",
"cmd.view_navigate": "Navegar na vista",
"cmd.view_rename": "Renomear vista nomeada",
"cmd.view_right": "Vista direita",
"cmd.view_save": "Nova vista nomeada",
"cmd.view_top": "Vista superior",
"color.copy": "Clique para copiar os valores da cor",
"color.edit": "Clique para editar a cor",
"color.gamma": "Mostrando valores de cor no espaço gama 0-255",
@@ -856,19 +875,57 @@
"help.mit": "Licença MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenças de terceiros",
"home.back": "Voltar para {name}",
"home.crashes": "Relatórios de falhas",
"home.design_workspace": "Projeto",
"home.documentation": "Documentação",
"home.export": "Exportar",
"home.export_hint": "Escolha um formato para o projeto atual.",
"home.export_json_hint": "Projeto editável e histórico de comandos",
"home.export_mesh_hint": "Malha para impressão ou compartilhamento",
"home.export_step_hint": "Geometria sólida precisa",
"home.import": "Importar…",
"home.import_hint": "STEP, IGES ou malha em um novo projeto",
"home.in_this_browser": "neste navegador",
"home.missing": "ausente",
"home.month_01": "jan.",
"home.month_02": "fev.",
"home.month_03": "mar.",
"home.month_04": "abr.",
"home.month_05": "mai.",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "ago.",
"home.month_09": "set.",
"home.month_10": "out.",
"home.month_11": "nov.",
"home.month_12": "dez.",
"home.new_inches": "Novo projeto em polegadas",
"home.new_inches_hint": "Polegadas · abre em Projeto",
"home.new_mm": "Novo projeto",
"home.new_mm_hint": "Milímetros · abre em Projeto",
"home.open": "Abrir…",
"home.opened_days_ago": "Aberto há {count} dias",
"home.opened_hours_ago": "Aberto há {count} h",
"home.opened_just_now": "Aberto agora mesmo",
"home.opened_minutes_ago": "Aberto há {count} min",
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Há {count} dias",
"home.opened_hours_ago": "Há {count} h",
"home.opened_just_now": "Agora mesmo",
"home.opened_minutes_ago": "Há {count} min",
"home.opening": "abrindo…",
"home.properties": "Propriedades do projeto",
"home.recent": "Recentes",
"home.recent_empty": "Os projetos que você abrir ou salvar aparecerão aqui.",
"home.recovery_count": "Projetos recuperáveis: {count}",
"home.recovery_empty": "Nenhum projeto recuperável.",
"home.recovery_hint": "O SolidCraft foi fechado antes que estes projetos fossem salvos.",
"home.report_issue": "Relatar um problema",
"home.samples": "Exemplos",
"home.settings": "Configurações",
"home.short_date": "{day} {month}",
"home.shortcuts": "Atalhos de teclado",
"home.start_hint": "Crie um projeto, abra um arquivo recente ou explore um exemplo.",
"home.whats_new": "Novidades",
"home.yesterday": "Ontem",
"install.button_tip": "Instale o SolidCraft como aplicativo: ele abre em sua própria janela e funciona offline",
"install.dock_1": "Escolha Arquivo › Adicionar ao Dock.",
"install.dock_2": "Clique em Adicionar.",
@@ -1021,7 +1078,7 @@
"search.try": "Digite o nome de um comando, um recurso deste projeto ou um parâmetro. Para executar um comando com valores, digite seu id e um objeto JSON: {example}",
"search.valid": "Válido",
"settings.display_quality_note": "Níveis mais finos desenham faces e arestas curvas mais suaves e demoram mais para gerar a malha. Só a janela de visualização muda, nunca o projeto ou as exportações.",
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista. Enquanto estiver ativo, a vista é redesenhada continuamente.",
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista.",
"settings.measure_axis_distances": "Mostrar distâncias X/Y/Z",
"settings.measure_axis_distances_help": "Mostrar as componentes nos eixos do mundo junto com a distância total enquanto Medir está aberto.",
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
@@ -1082,7 +1139,106 @@
"trace.stopped_by_contact": "Termina na amostra {sample}: {a} atravessaria {b}",
"trace.stopped_by_loop": "Termina na amostra {sample}: o laço por {joint} não pode fechar mais",
"ui.1_degree_of_freedom": "1 grau de liberdade",
"ui.3d_mouse_axes_reassigned": "Os eixos estão reatribuídos nas Preferências, e isso continua valendo sobre esta calibração.",
"ui.3d_mouse_button": "Botão {number}",
"ui.3d_mouse_button_nothing": "Nada",
"ui.3d_mouse_button_settings": "Configurações do mouse 3D",
"ui.3d_mouse_buttons": "Botões",
"ui.3d_mouse_buttons_hint": "Aperte um botão do seu mouse 3D para encontrá-lo aqui e escolha o que ele faz.",
"ui.3d_mouse_buttons_no_device": "Conecte um mouse 3D para escolher o que os botões fazem.",
"ui.3d_mouse_buttons_reset": "Restaurar os botões padrão",
"ui.3d_mouse_calibrate": "Calibrar…",
"ui.3d_mouse_calibrate_tip": "Mova a tampa como a janela pedir, para que o SolidCraft aprenda para onde e até onde vai cada eixo deste mouse 3D. Salvo para este mouse 3D.",
"ui.3d_mouse_calibrate_title": "Calibrar mouse 3D",
"ui.3d_mouse_calibrate_wait": "Mova o mouse 3D uma vez para o SolidCraft vê-lo.",
"ui.3d_mouse_calibrated": "Calibrado",
"ui.3d_mouse_calibration": "Calibração do mouse 3D",
"ui.3d_mouse_calibration_done": "Pronto. Salve para usá-la sempre que este mouse 3D estiver conectado.",
"ui.3d_mouse_calibration_no_response": "Um eixo quase não se moveu, então não dá para calibrá-lo. Recomece e mova a tampa mais longe a cada vez.",
"ui.3d_mouse_circles_help": "Os círculos mostram o que o dispositivo envia após a calibração. O modelo de teste mostra o que a vista faz com isso, após inversão, velocidade e atribuição de eixos.",
"ui.3d_mouse_dead_zone": "Zona morta",
"ui.3d_mouse_dead_zone_tip": "Quanto a tampa precisa se mover antes de a vista se mover, como fração do curso completo. Aumente se a vista se mexer com a tampa parada.",
"ui.3d_mouse_denied": "O SolidCraft vê um mouse 3D, mas não tem permissão para lê-lo. A regra udev está nas notas de configuração do mouse 3D.",
"ui.3d_mouse_denied_short": "Encontrado, mas não legível",
"ui.3d_mouse_driven_by": "Qual movimento calibrado do dispositivo controla este eixo. Escolher um que outro eixo já usa troca os dois.",
"ui.3d_mouse_driven_by_label": "controlado por",
"ui.3d_mouse_driver": "Driver do mouse 3D",
"ui.3d_mouse_driver_auto": "Automático",
"ui.3d_mouse_driver_direct": "Direto (hidraw)",
"ui.3d_mouse_driver_fallback": "Lendo por enquanto pelo spacenavd: não foi possível abrir o dispositivo em si.",
"ui.3d_mouse_driver_only_direct": "Lendo o dispositivo diretamente; o spacenavd não está em execução",
"ui.3d_mouse_driver_only_spacenavd": "Lendo pelo spacenavd, a única opção disponível",
"ui.3d_mouse_forget": "Esquecer calibração",
"ui.3d_mouse_ghost_left_viewport": "Viewport à esquerda",
"ui.3d_mouse_ghost_left_window": "Janela à esquerda",
"ui.3d_mouse_ghost_position": "Posição",
"ui.3d_mouse_ghost_right_viewport": "Viewport à direita",
"ui.3d_mouse_hint": "Move e gira a vista juntos; esboços usam apenas deslocamento e zoom.",
"ui.3d_mouse_kind_cap": "Retorno ao centro",
"ui.3d_mouse_kind_question": "Que tipo de mouse 3D é?",
"ui.3d_mouse_kind_trackball": "Trackball",
"ui.3d_mouse_looks_like_cap": "O valor se mantém enquanto você segura, como em um dispositivo do tipo “Retorno ao centro”.",
"ui.3d_mouse_looks_like_trackball": "O valor volta assim que você para de mover, como em um trackball.",
"ui.3d_mouse_motion_rotation_x": "Rotação X",
"ui.3d_mouse_motion_rotation_y": "Rotação Y",
"ui.3d_mouse_motion_rotation_z": "Rotação Z",
"ui.3d_mouse_motion_translation_x": "Translação X",
"ui.3d_mouse_motion_translation_y": "Translação Y",
"ui.3d_mouse_motion_translation_z": "Translação Z",
"ui.3d_mouse_navigate": "Navegar com o mouse 3D",
"ui.3d_mouse_none": "Nenhum mouse 3D encontrado",
"ui.3d_mouse_not_calibrated": "Não calibrado",
"ui.3d_mouse_offer": "Calibrar o mouse 3D corrige eixos trocados ou invertidos e ajusta a velocidade e a sensação.",
"ui.3d_mouse_offer_never": "Não mostrar novamente",
"ui.3d_mouse_offer_swap": "Mudar para Hidraw (driver direto)",
"ui.3d_mouse_reading": "Leitura",
"ui.3d_mouse_reset_axes": "Redefinir atribuição de eixos",
"ui.3d_mouse_reset_view": "Redefinir visualização",
"ui.3d_mouse_restore": "Restaurar padrões do mouse 3D",
"ui.3d_mouse_restore_ask": "Voltar as configurações do mouse 3D aos padrões? A calibração e o mapeamento dos botões continuam.",
"ui.3d_mouse_section": "Mouse 3D",
"ui.3d_mouse_show_in_view": "Mostrar o mouse 3D na vista 3D",
"ui.3d_mouse_show_in_view_tip": "Uma imagem do seu mouse 3D no canto inferior esquerdo, que se move quando você o move.",
"ui.3d_mouse_skip": "Meu dispositivo não faz isso",
"ui.3d_mouse_skip_tip": "Pula este movimento nos dois sentidos; a calibração o deixa de fora.",
"ui.3d_mouse_speed": "Velocidade do mouse 3D",
"ui.3d_mouse_step_lift": "Levante a tampa, segure e solte.",
"ui.3d_mouse_step_press": "Pressione a tampa para baixo, segure e solte.",
"ui.3d_mouse_step_pull": "Puxe a tampa na sua direção, segure e solte.",
"ui.3d_mouse_step_push": "Empurre a tampa para longe de você, segure e solte.",
"ui.3d_mouse_step_rest": "Tire a mão da tampa e deixe-a parada.",
"ui.3d_mouse_step_rest_trackball": "Tire a mão da bola e deixe-a parada.",
"ui.3d_mouse_step_ring_clockwise": "Gire o anel de rolagem alguns cliques no sentido horário.",
"ui.3d_mouse_step_ring_counterclockwise": "Gire o anel de rolagem alguns cliques no sentido anti-horário.",
"ui.3d_mouse_step_roll_away": "Role a bola para longe de você algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_roll_left": "Role a bola para a esquerda algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_roll_right": "Role a bola para a direita algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_roll_toward": "Role a bola na sua direção algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_slide_left": "Deslize a tampa para a esquerda, segure e solte.",
"ui.3d_mouse_step_slide_right": "Deslize a tampa para a direita, segure e solte.",
"ui.3d_mouse_step_spin_clockwise": "Gire a bola no sentido horário algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_spin_counterclockwise": "Gire a bola no sentido anti-horário algumas vezes e deixe-a parar.",
"ui.3d_mouse_step_tilt_away": "Incline o topo da tampa para longe de você (a borda mais próxima sobe), segure e solte.",
"ui.3d_mouse_step_tilt_left": "Incline a tampa para a esquerda (borda esquerda para baixo), segure e solte.",
"ui.3d_mouse_step_tilt_right": "Incline a tampa para a direita (borda direita para baixo), segure e solte.",
"ui.3d_mouse_step_tilt_toward": "Incline o topo da tampa na sua direção (a borda mais próxima desce), segure e solte.",
"ui.3d_mouse_step_touch": "Apoie a mão na tampa sem empurrá-la.",
"ui.3d_mouse_step_twist_clockwise": "Gire a tampa no sentido horário, segure e solte.",
"ui.3d_mouse_step_twist_counterclockwise": "Gire a tampa no sentido anti-horário, segure e solte.",
"ui.3d_mouse_switch_cap": "Calibrar como retorno ao centro",
"ui.3d_mouse_switch_trackball": "Calibrar como trackball",
"ui.3d_mouse_view_axis": "Eixo da vista",
"ui.3d_mouse_view_off": "Desligado",
"ui.3d_mouse_view_off_tip": "O perfil do driver do mouse 3D desliga este eixo, então o movimento dele não move a vista. Escolher aqui o movimento de outro eixo troca os dois, e esse movimento deixa de mover a vista.",
"ui.3d_mouse_view_orbit_sideways": "Orbitar esquerda / direita",
"ui.3d_mouse_view_orbit_up_down": "Orbitar cima / baixo",
"ui.3d_mouse_view_pan_sideways": "Deslocar esquerda / direita",
"ui.3d_mouse_view_pan_up_down": "Deslocar cima / baixo",
"ui.3d_mouse_view_roll": "Girar a vista",
"ui.3d_mouse_view_unmapped": "Eixo {n}",
"ui.3d_mouse_view_zoom": "Zoom",
"ui.3d_mouse_waiting": "Aguardando movimento",
"ui.3d_mouse_wiggle": "Se você tiver um conectado, mexa nele um pouco.",
"ui.3d_sketch": "Esboço 3D",
"ui.activate": "Ativar",
"ui.active_component": "Componente ativo",
@@ -1092,6 +1248,7 @@
"ui.agent_chat_add": "Anexar arquivos",
"ui.agent_chat_add_file": "Escolher um arquivo…",
"ui.agent_chat_allow": "Permitir",
"ui.agent_chat_allow_once": "Permitir uma vez",
"ui.agent_chat_allowed": "Permitido",
"ui.agent_chat_answered": "Resposta: {option}",
"ui.agent_chat_approval": "Permitir que o agente faça isto?",
@@ -1143,12 +1300,16 @@
"ui.agent_chat_files_left_out": "No máximo {max} arquivos acompanham uma mensagem; deixados de fora: {files}",
"ui.agent_chat_files_loading": "Aguardando a leitura dos arquivos",
"ui.agent_chat_files_max": "No máximo {max} arquivos por mensagem",
"ui.agent_chat_flatpak_none_found": "Nenhum agente de programação encontrado. Por padrão, a sandbox do Flatpak não vê agentes instalados na sua pasta pessoal e não tem acesso à rede. Para permitir o Claude Code e o adaptador ACP dele instalados via nvm ou em ~/.local/bin, execute este comando em um terminal e reinicie o SolidCraft. Ele concede acesso aos programas e arquivos de login listados, além de acesso à rede:",
"ui.agent_chat_import_failed": "{name}: falha na importação: {error}",
"ui.agent_chat_intro": "Converse com um agente de programação que você já usa, ou com um endpoint de modelo que você escolher. Ele trabalha no projeto aberto pelas ferramentas do SolidCraft: as alterações aparecem conforme são feitas e se desfazem em um passo por mensagem. O que ele pode fazer sem perguntar é definido em Preferências > IA.",
"ui.agent_chat_key_in_env": "chave definida",
"ui.agent_chat_left": "Esquerda",
"ui.agent_chat_limit": "O agente atingiu o limite de {limit} alterações por mensagem. Envie outra mensagem para que ele continue.",
"ui.agent_chat_minimize": "Minimizar para uma faixa",
"ui.agent_chat_mode_all": "Tudo",
"ui.agent_chat_mode_ask": "Perguntar",
"ui.agent_chat_mode_edits": "Edições",
"ui.agent_chat_model_tip": "O modelo que o agente usa. Sua escolha é lembrada para este agente.",
"ui.agent_chat_new": "Novo chat",
"ui.agent_chat_new_confirm": "Começar um novo chat? O atual será apagado.",
@@ -1161,6 +1322,7 @@
"ui.agent_chat_none_found_short": "Nenhum agente encontrado",
"ui.agent_chat_not_installed": "Não instalado",
"ui.agent_chat_not_signed_in": "não conectado",
"ui.agent_chat_open_settings": "Abrir preferências",
"ui.agent_chat_own_window": "Janela própria",
"ui.agent_chat_paste_picture": "Colar uma imagem",
"ui.agent_chat_permission": "O agente pede para executar: {title}",
@@ -1185,6 +1347,8 @@
"ui.agent_chat_send": "Enviar",
"ui.agent_chat_send_now": "Enviar agora",
"ui.agent_chat_send_now_tip": "Parar o passo atual do agente e enviar esta em seguida (Ctrl+Enter)",
"ui.agent_chat_setup_body": "O SolidCraft se comunica com um agente de programação instalado neste computador. Escolha um em Preferências › IA.",
"ui.agent_chat_setup_title": "Configurar um assistente",
"ui.agent_chat_side": "Lado do chat com agente",
"ui.agent_chat_signed_in": "conectado",
"ui.agent_chat_state_auth": "É preciso entrar",
@@ -1201,6 +1365,7 @@
"ui.agent_chat_undo_label": "Chat com agente: {message}",
"ui.agent_chat_undone": "(desfeito)",
"ui.agent_chat_view_attached": "Vista anexada",
"ui.agent_chat_waiting": "Aguardando sua resposta",
"ui.agent_cursor": "Cursor do agente",
"ui.agent_cursor_speed": "Velocidade do cursor do agente",
"ui.ai_add": "Adicionar",
@@ -1258,7 +1423,6 @@
"ui.ai_permissions": "Permissões",
"ui.ai_preset": "Predefinição",
"ui.ai_remove": "Remover",
"ui.ai_request_limit": "parou após 24 requisições para uma mensagem; envie outra mensagem para continuar",
"ui.ai_reset": "Redefinir",
"ui.ai_settings": "Configurações de IA…",
"ui.ai_show_features": "Mostrar recursos de IA",
@@ -1303,6 +1467,7 @@
"ui.blocked": "Bloqueado",
"ui.bodies": "Corpos",
"ui.bodies_all_if_none": "Corpos (todos se nenhum)",
"ui.bottom": "Base",
"ui.box": "Caixa",
"ui.break": "Quebrar",
"ui.browser_bodies_one": "{count} corpo",
@@ -1455,6 +1620,7 @@
"ui.grid_follows_view_hint": "As vistas laterais mostram a grade no plano vertical voltado para elas, em vez do solo",
"ui.ground_shadow": "Sombra no chão",
"ui.groupSelected": "Agrupar seleção",
"ui.hatch": "Hachura",
"ui.heading_add_ins": "COMPLEMENTOS",
"ui.heading_assemble": "MONTAR",
"ui.heading_box": "CAIXA",
@@ -1528,6 +1694,11 @@
"ui.hideOrigin": "Ocultar origem",
"ui.hidePlanes": "Ocultar planos",
"ui.hideProfile": "Ocultar perfil",
"ui.history_back": "Voltar uma etapa",
"ui.history_below_browser": "Abaixo do navegador",
"ui.history_first": "Ir para o início",
"ui.history_forward": "Avançar uma etapa",
"ui.history_last": "Ir para o fim",
"ui.history_title": "Histórico",
"ui.hole": "Furo",
"ui.hole_tap_type": "Tipo de rosca do furo",
@@ -1545,6 +1716,8 @@
"ui.joints": "Juntas",
"ui.keyboard_shortcuts": "Atalhos de teclado",
"ui.language": "Idioma",
"ui.layout_compact": "Compacto",
"ui.layout_floating": "Flutuante",
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
@@ -1682,6 +1855,7 @@
"ui.reverse_zoom_direction": "Inverter direção do zoom",
"ui.revolve": "Revolução",
"ui.ribbon_exit": "Sair",
"ui.ribbon_labels": "Mostrar texto abaixo dos ícones da faixa de opções",
"ui.ribbon_pin_footer": "Os comandos fixados aparecem na faixa de opções. Passe o mouse sobre uma linha para fixar ou desafixar.",
"ui.ribbon_workspace": "Espaço de trabalho",
"ui.right": "Direita",
@@ -1737,34 +1911,6 @@
"ui.smooth_wheel_zoom": "Zoom suave com a roda",
"ui.smooth_wheel_zoom_tip": "Desative para aplicar cada evento da roda na hora, sem rastro de suavização",
"ui.snap": "Encaixe",
"ui.space_mouse_calibrate": "Calibrar…",
"ui.space_mouse_calibrate_tip": "Mova a tampa como a janela pedir, para que o SolidCraft aprenda para onde e até onde vai cada eixo deste mouse 3D. Salvo para este mouse 3D.",
"ui.space_mouse_calibrate_title": "Calibrar mouse 3D",
"ui.space_mouse_calibrated": "Calibrado",
"ui.space_mouse_calibration": "Calibração do mouse 3D",
"ui.space_mouse_calibration_done": "Pronto. Salve para usá-la sempre que este mouse 3D estiver conectado.",
"ui.space_mouse_calibration_no_response": "Um eixo quase não se moveu, então não dá para calibrá-lo. Recomece e mova a tampa mais longe a cada vez.",
"ui.space_mouse_dead_zone": "Zona morta",
"ui.space_mouse_dead_zone_tip": "Quanto a tampa precisa se mover antes de a vista se mover, como fração do curso completo. Aumente se a vista se mexer com a tampa parada.",
"ui.space_mouse_forget": "Esquecer calibração",
"ui.space_mouse_hint": "Move e gira a vista juntos; esboços usam apenas deslocamento e zoom.",
"ui.space_mouse_navigate": "Navegar com SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Nenhum mouse 3D encontrado",
"ui.space_mouse_not_calibrated": "Não calibrado",
"ui.space_mouse_reading": "Leitura",
"ui.space_mouse_step_lift": "Levante a tampa, segure e solte.",
"ui.space_mouse_step_press": "Pressione a tampa para baixo, segure e solte.",
"ui.space_mouse_step_pull": "Puxe a tampa na sua direção, segure e solte.",
"ui.space_mouse_step_push": "Empurre a tampa para longe de você, segure e solte.",
"ui.space_mouse_step_rest": "Tire a mão da tampa e deixe-a parada.",
"ui.space_mouse_step_slide_left": "Deslize a tampa para a esquerda, segure e solte.",
"ui.space_mouse_step_slide_right": "Deslize a tampa para a direita, segure e solte.",
"ui.space_mouse_step_tilt_back": "Incline a tampa para trás (borda da frente para cima), segure e solte.",
"ui.space_mouse_step_tilt_forward": "Incline a tampa para a frente (borda da frente para baixo), segure e solte.",
"ui.space_mouse_step_tilt_left": "Incline a tampa para a esquerda (borda esquerda para baixo), segure e solte.",
"ui.space_mouse_step_tilt_right": "Incline a tampa para a direita (borda direita para baixo), segure e solte.",
"ui.space_mouse_step_twist_clockwise": "Gire a tampa no sentido horário, segure e solte.",
"ui.space_mouse_step_twist_counterclockwise": "Gire a tampa no sentido anti-horário, segure e solte.",
"ui.spacing": "Espaçamento",
"ui.spacing_2": "Espaçamento 2",
"ui.sphere": "Esfera",
@@ -1836,6 +1982,7 @@
"ui.workspace_design_tabs": "Modelo · Superfície · Montagem · Inserir · Inspecionar · Gerenciar · Vista",
"ui.workspace_drawing_description": "Desenhos 2D com vistas, cotas e legendas",
"ui.workspace_footer": "Todo espaço de trabalho usa o mesmo projeto, histórico e seleção; só as guias da faixa de opções mudam. O seletor fica bloqueado enquanto um esboço ou recurso está sendo editado.",
"ui.workspace_layout": "Layout do espaço de trabalho",
"ui.workspace_manufacture_description": "Fatiamento, trajetórias de ferramenta e configuração",
"ui.workspace_planned": "Planejado",
"ui.workspace_render_description": "Pré-visualização do produto, configurações de cena e imagens renderizadas",
+117 -35
View File
@@ -72,6 +72,7 @@
"Choose…": "Alegeți…",
"Circular": "Circular",
"Circumference": "Circumferință",
"Clockwise": "În sens orar",
"Close draft": "Închide ciorna",
"Collisions": "Coliziuni",
"Colour": "Culoare",
@@ -167,6 +168,7 @@
"HISTORY": "ISTORIC",
"Height": "Înălțime",
"Hidden edges": "Muchii ascunse",
"History layout": "Aspectul istoricului",
"Holes": "Găuri",
"Holes (edges on them)": "Găuri (muchiile lor)",
"Home view": "Vizualizare de start",
@@ -232,7 +234,6 @@
"No crash reports.": "Niciun raport de blocare.",
"No matching appearance presets.": "Niciun preset de aspect corespunzător.",
"No matching appearances": "Niciun aspect corespunzător",
"No visible bodies to export.": "Niciun corp vizibil de exportat.",
"None": "Niciunul",
"Not available yet": "Indisponibil deocamdată",
"Notch": "Crestătură",
@@ -321,6 +322,7 @@
"Search commands": "Caută comenzi",
"Search this library": "Caută în această bibliotecă",
"Section": "Secțiune",
"Section position": "Poziția secțiunii",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Selectează altul: țineți apăsat butonul stâng nemișcat pe model (sau clic dreapta › Selectează altul…) pentru a lista tot ce se află sub cursor; faceți clic din nou în același loc pentru a selecta următorul element. Dublu clic pe butonul din mijloc pentru a merge Acasă (Preferințe › Navigare).",
"Select bodies (a face picks its body)": "Selectați corpuri (o față alege corpul ei)",
"Select bodies or faces instead": "Selectați în schimb corpuri sau fețe",
@@ -332,6 +334,7 @@
"Show Agent Cursor": "Afișează cursorul agentului",
"Show in folder": "Afișează în dosar",
"Show the pointer an AI agent moves (#35)": "Afișează indicatorul pe care îl mută un agent AI (#35)",
"Show tool at cursor": "Afișează instrumentul lângă cursor",
"Signed axes": "Axe cu semn",
"Size": "Dimensiune",
"Sketch constraints": "Constrângerile schiței",
@@ -344,9 +347,11 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate. Ultima lor salvare automată poate fi deschisă.",
"Specialise": "Specializează",
"Speed": "Viteză",
"Square": "Pătrat",
"Stand it up": "Ridică-l în picioare",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Ridică-l în picioare: un sfert de rotație în jurul lui X, ca o singură operație de mutare ce poate fi anulată.",
"Stand this part up?": "Ridicați această piesă în picioare?",
"Start angle": "Unghi de început",
"Start bulge": "Bombare inițială",
"Start continuity": "Continuitate inițială",
"Step": "Pas",
@@ -411,6 +416,7 @@
"angle, height": "unghi, înălțime",
"appearance.dielectric_ior": "IOR dielectric",
"appearance.metallic": "Metalic",
"appearance.only_available": "Doar aspecte disponibile",
"appearance.pbr_help": "Vizualizați această suprafață în Randare sau într-un PNG de produs. Fără refracție sau transmisie.",
"appearance.pbr_opaque": "PBR opac",
"appearance.roughness": "Rugozitate",
@@ -529,6 +535,10 @@
"cmd.file_insert_step": "Inserează STEP",
"cmd.file_save_mesh": "Salvează ca plasă",
"cmd.file_slicers": "Slicere instalate",
"cmd.frame_member": "Element de cadru",
"cmd.frame_profile_insert": "Inserare profil",
"cmd.frame_profile_list": "Bibliotecă de profile",
"cmd.frame_profile_properties": "Proprietăți profil",
"cmd.inspect_accessibility": "Analiza accesibilității",
"cmd.inspect_center_of_mass": "Centru de masă",
"cmd.inspect_clearance": "Joc",
@@ -568,6 +578,7 @@
"cmd.material_create": "Material fizic nou",
"cmd.material_delete": "Șterge materialul fizic",
"cmd.material_edit": "Editează materialul fizic",
"cmd.material_inspect": "Inspectează materialul fizic",
"cmd.material_list": "Listează materialele",
"cmd.material_properties_directional": "Modul de elasticitate direcțional",
"cmd.material_properties_validate": "Validează datele materialului fizic",
@@ -856,19 +867,57 @@
"help.mit": "Licența MIT",
"help.notice": "NOTICE",
"help.third_party": "Licențe terțe",
"home.back": "Înapoi la {name}",
"home.crashes": "Rapoarte de blocare",
"home.design_workspace": "Proiectare",
"home.documentation": "Documentație",
"home.export": "Exportă",
"home.export_hint": "Alege un format pentru proiectul curent.",
"home.export_json_hint": "Proiect editabil și istoricul comenzilor",
"home.export_mesh_hint": "Plasă pentru imprimare sau partajare",
"home.export_step_hint": "Geometrie solidă precisă",
"home.import": "Importă…",
"home.import_hint": "STEP, IGES sau plasă într-un proiect nou",
"home.in_this_browser": "în acest browser",
"home.missing": "lipsă",
"home.month_01": "ian.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "apr.",
"home.month_05": "mai",
"home.month_06": "iun.",
"home.month_07": "iul.",
"home.month_08": "aug.",
"home.month_09": "sept.",
"home.month_10": "oct.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Proiect nou în inci",
"home.new_inches_hint": "Inci · se deschide în Proiectare",
"home.new_mm": "Proiect nou",
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
"home.open": "Deschide…",
"home.opened_days_ago": "Deschis acum {count} zile",
"home.opened_hours_ago": "Deschis acum {count} h",
"home.opened_just_now": "Deschis chiar acum",
"home.opened_minutes_ago": "Deschis acum {count} min",
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
"home.open_now": "Deschis acum",
"home.opened_days_ago": "Acum {count} zile",
"home.opened_hours_ago": "Acum {count} h",
"home.opened_just_now": "Chiar acum",
"home.opened_minutes_ago": "Acum {count} min",
"home.opening": "se deschide…",
"home.properties": "Proprietățile proiectului",
"home.recent": "Recente",
"home.recent_empty": "Proiectele pe care le deschizi sau le salvezi apar aici.",
"home.recovery_count": "Proiecte recuperabile: {count}",
"home.recovery_empty": "Nu există proiecte recuperabile.",
"home.recovery_hint": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate.",
"home.report_issue": "Raportează o problemă",
"home.samples": "Exemple",
"home.settings": "Setări",
"home.short_date": "{day} {month}",
"home.shortcuts": "Scurtături de la tastatură",
"home.start_hint": "Creează un proiect, deschide un fișier recent sau explorează un exemplu.",
"home.whats_new": "Noutăți",
"home.yesterday": "Ieri",
"install.button_tip": "Instalați SolidCraft ca aplicație: se deschide în propria fereastră și funcționează offline",
"install.dock_1": "Alegeți Fișier › Adaugă în Dock.",
"install.dock_2": "Faceți clic pe Adaugă.",
@@ -1021,7 +1070,7 @@
"search.try": "Tastați numele unei comenzi, o operație din acest proiect sau un parametru. Pentru a executa o comandă cu valori, tastați id-ul ei și un obiect JSON: {example}",
"search.valid": "Valid",
"settings.display_quality_note": "Nivelurile mai fine desenează fețele și muchiile curbe mai neted și durează mai mult la generarea plasei. Se schimbă doar fereastra de vizualizare, niciodată proiectul sau exporturile.",
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării. Cât timp este activat, vizualizarea se redesenează continuu.",
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării.",
"settings.measure_axis_distances": "Afișează distanțele X/Y/Z",
"settings.measure_axis_distances_help": "Afișează cotele pe axele globale alături de distanța totală cât timp Măsurare este deschis.",
"settings.msaa_applies_on_restart": "Anti-aliasing se aplică la următoarea pornire a SolidCraft",
@@ -1082,7 +1131,50 @@
"trace.stopped_by_contact": "Se termină la eșantionul {sample}: {a} ar trece prin {b}",
"trace.stopped_by_loop": "Se termină la eșantionul {sample}: bucla prin {joint} nu se mai poate închide",
"ui.1_degree_of_freedom": "1 grad de libertate",
"ui.3d_mouse_calibrate": "Calibrează…",
"ui.3d_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
"ui.3d_mouse_calibrate_title": "Calibrează mouse-ul 3D",
"ui.3d_mouse_calibrate_wait": "Mișcă o dată mouse-ul 3D ca SolidCraft să îl vadă.",
"ui.3d_mouse_calibrated": "Calibrat",
"ui.3d_mouse_calibration": "Calibrarea mouse-ului 3D",
"ui.3d_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
"ui.3d_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
"ui.3d_mouse_circles_help": "Cercurile arată ce trimite dispozitivul după calibrare. Modelul de test arată ce face vizualizarea cu acestea, după inversare, viteză și atribuirea axelor.",
"ui.3d_mouse_dead_zone": "Zonă moartă",
"ui.3d_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D, dar nu are voie să îl citească. Regula udev este în notele de configurare a mouse-ului 3D.",
"ui.3d_mouse_denied_short": "Găsit, dar nu poate fi citit",
"ui.3d_mouse_driver_fallback": "Deocamdată se citește prin spacenavd: dispozitivul în sine nu a putut fi deschis.",
"ui.3d_mouse_forget": "Uită calibrarea",
"ui.3d_mouse_ghost_left_viewport": "Viewport stânga",
"ui.3d_mouse_ghost_left_window": "Fereastră stânga",
"ui.3d_mouse_ghost_position": "Poziție",
"ui.3d_mouse_ghost_right_viewport": "Viewport dreapta",
"ui.3d_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
"ui.3d_mouse_navigate": "Navighează cu mouse-ul 3D",
"ui.3d_mouse_none": "Niciun mouse 3D găsit",
"ui.3d_mouse_not_calibrated": "Necalibrat",
"ui.3d_mouse_offer": "Calibrarea mouse-ului 3D corectează axele inversate sau schimbate între ele și îi reglează viteza și senzația.",
"ui.3d_mouse_offer_never": "Nu mai afișa",
"ui.3d_mouse_offer_swap": "Treci la Hidraw (driver direct)",
"ui.3d_mouse_reading": "Valoare citită",
"ui.3d_mouse_reset_view": "Resetează vizualizarea",
"ui.3d_mouse_restore": "Restaurează implicitele mouse-ului 3D",
"ui.3d_mouse_restore_ask": "Revii la valorile implicite ale setărilor mouse-ului 3D? Calibrarea și atribuirea butoanelor rămân.",
"ui.3d_mouse_speed": "Viteza mouse-ului 3D",
"ui.3d_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
"ui.3d_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
"ui.3d_mouse_waiting": "Se așteaptă mișcare",
"ui.3d_mouse_wiggle": "Dacă ai unul conectat, mișcă-l puțin.",
"ui.3d_sketch": "Schiță 3D",
"ui.activate": "Activează",
"ui.active_component": "Componentă activă",
@@ -1092,6 +1184,7 @@
"ui.agent_chat_add": "Atașează fișiere",
"ui.agent_chat_add_file": "Alegeți un fișier…",
"ui.agent_chat_allow": "Permite",
"ui.agent_chat_allow_once": "Permite o dată",
"ui.agent_chat_allowed": "Permis",
"ui.agent_chat_answered": "Răspuns: {option}",
"ui.agent_chat_approval": "Permiteți agentului să facă asta?",
@@ -1143,12 +1236,16 @@
"ui.agent_chat_files_left_out": "Cel mult {max} fișiere merg cu un mesaj; omise: {files}",
"ui.agent_chat_files_loading": "Se așteaptă citirea fișierelor",
"ui.agent_chat_files_max": "Cel mult {max} fișiere pe un mesaj",
"ui.agent_chat_flatpak_none_found": "Nu s-a găsit niciun agent de programare. Implicit, sandboxul Flatpak nu vede agenții instalați în dosarul personal și nu are acces la rețea. Pentru a permite Claude Code și adaptorul său ACP instalate prin nvm sau în ~/.local/bin, rulați această comandă într-un terminal, apoi reporniți SolidCraft. Ea acordă acces la programele și fișierele de autentificare enumerate, precum și acces la rețea:",
"ui.agent_chat_import_failed": "{name}: importul a eșuat: {error}",
"ui.agent_chat_intro": "Conversați cu un agent de programare pe care îl folosiți deja sau cu un endpoint de model ales de dumneavoastră. Lucrează la proiectul deschis prin instrumentele SolidCraft: modificările lui apar pe măsură ce le face și se anulează ca un singur pas per mesaj. Ce poate face fără să întrebe se stabilește în Preferințe > AI.",
"ui.agent_chat_key_in_env": "cheie setată",
"ui.agent_chat_left": "Stânga",
"ui.agent_chat_limit": "Agentul a atins limita de {limit} modificări pentru un mesaj. Trimiteți alt mesaj pentru a-l lăsa să continue.",
"ui.agent_chat_minimize": "Minimizează într-o bandă",
"ui.agent_chat_mode_all": "Tot",
"ui.agent_chat_mode_ask": "Întreabă",
"ui.agent_chat_mode_edits": "Modificări",
"ui.agent_chat_model_tip": "Modelul folosit de agent. Alegerea dumneavoastră este reținută pentru acest agent.",
"ui.agent_chat_new": "Chat nou",
"ui.agent_chat_new_confirm": "Începeți un chat nou? Cel curent este șters.",
@@ -1161,6 +1258,7 @@
"ui.agent_chat_none_found_short": "Niciun agent găsit",
"ui.agent_chat_not_installed": "Neinstalat",
"ui.agent_chat_not_signed_in": "neautentificat",
"ui.agent_chat_open_settings": "Deschide preferințele",
"ui.agent_chat_own_window": "Fereastră proprie",
"ui.agent_chat_paste_picture": "Lipește o imagine",
"ui.agent_chat_permission": "Agentul cere să ruleze: {title}",
@@ -1185,6 +1283,8 @@
"ui.agent_chat_send": "Trimite",
"ui.agent_chat_send_now": "Trimite acum",
"ui.agent_chat_send_now_tip": "Oprește pasul curent al agentului și trimite acest mesaj următorul (Ctrl+Enter)",
"ui.agent_chat_setup_body": "SolidCraft comunică cu un agent de programare instalat pe acest computer. Alege unul în Preferințe › AI.",
"ui.agent_chat_setup_title": "Configurează un asistent",
"ui.agent_chat_side": "Partea chatului cu agentul",
"ui.agent_chat_signed_in": "autentificat",
"ui.agent_chat_state_auth": "Autentificare necesară",
@@ -1201,6 +1301,7 @@
"ui.agent_chat_undo_label": "Chat cu agentul: {message}",
"ui.agent_chat_undone": "(anulat)",
"ui.agent_chat_view_attached": "Vizualizare atașată",
"ui.agent_chat_waiting": "Așteaptă răspunsul tău",
"ui.agent_cursor": "Cursorul agentului",
"ui.agent_cursor_speed": "Viteza cursorului agentului",
"ui.ai_add": "Adaugă",
@@ -1258,7 +1359,6 @@
"ui.ai_permissions": "Permisiuni",
"ui.ai_preset": "Presetare",
"ui.ai_remove": "Elimină",
"ui.ai_request_limit": "oprit după 24 de cereri pentru un mesaj; trimiteți alt mesaj pentru a continua",
"ui.ai_reset": "Resetează",
"ui.ai_settings": "Setări AI…",
"ui.ai_show_features": "Afișează funcțiile AI",
@@ -1455,6 +1555,7 @@
"ui.grid_follows_view_hint": "Vederile laterale afișează grila pe planul vertical orientat spre ele, în loc de sol",
"ui.ground_shadow": "Umbra la sol",
"ui.groupSelected": "Grupează selecția",
"ui.hatch": "Hașură",
"ui.heading_add_ins": "EXTENSII",
"ui.heading_assemble": "ASAMBLARE",
"ui.heading_box": "CUTIE",
@@ -1528,6 +1629,11 @@
"ui.hideOrigin": "Ascunde originea",
"ui.hidePlanes": "Ascunde planele",
"ui.hideProfile": "Ascunde profilul",
"ui.history_back": "Un pas înapoi",
"ui.history_below_browser": "Sub navigator",
"ui.history_first": "Mergi la început",
"ui.history_forward": "Un pas înainte",
"ui.history_last": "Mergi la sfârșit",
"ui.history_title": "Istoric",
"ui.hole": "Gaură",
"ui.hole_tap_type": "Tip de filetare gaură",
@@ -1545,6 +1651,8 @@
"ui.joints": "Îmbinări",
"ui.keyboard_shortcuts": "Scurtături de tastatură",
"ui.language": "Limbă",
"ui.layout_compact": "Compact",
"ui.layout_floating": "Plutitor",
"ui.length_decimals": "Zecimale lungime",
"ui.light": "Luminos",
"ui.line": "Linie",
@@ -1682,6 +1790,7 @@
"ui.reverse_zoom_direction": "Inversează direcția zoomului",
"ui.revolve": "Revoluție",
"ui.ribbon_exit": "Ieșire",
"ui.ribbon_labels": "Afișează text sub pictogramele panglicii",
"ui.ribbon_pin_footer": "Comenzile fixate apar pe bandă. Treceți cursorul peste un rând pentru a-l fixa sau defixa.",
"ui.ribbon_workspace": "Spațiu de lucru",
"ui.right": "Dreapta",
@@ -1737,34 +1846,6 @@
"ui.smooth_wheel_zoom": "Zoom lin cu rotița",
"ui.smooth_wheel_zoom_tip": "Dezactivați pentru a aplica imediat fiecare eveniment al rotiței, fără coadă de netezire",
"ui.snap": "Agățare",
"ui.space_mouse_calibrate": "Calibrează…",
"ui.space_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
"ui.space_mouse_calibrate_title": "Calibrează mouse-ul 3D",
"ui.space_mouse_calibrated": "Calibrat",
"ui.space_mouse_calibration": "Calibrarea mouse-ului 3D",
"ui.space_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
"ui.space_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
"ui.space_mouse_dead_zone": "Zonă moartă",
"ui.space_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
"ui.space_mouse_forget": "Uită calibrarea",
"ui.space_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
"ui.space_mouse_navigate": "Navighează cu SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Niciun mouse 3D găsit",
"ui.space_mouse_not_calibrated": "Necalibrat",
"ui.space_mouse_reading": "Valoare citită",
"ui.space_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
"ui.space_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_tilt_back": "Înclinați capacul spre spate (marginea din față în sus), țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_tilt_forward": "Înclinați capacul spre față (marginea din față în jos), țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
"ui.space_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
"ui.spacing": "Spațiere",
"ui.spacing_2": "Spațierea 2",
"ui.sphere": "Sferă",
@@ -1836,6 +1917,7 @@
"ui.workspace_design_tabs": "Model · Suprafață · Asamblare · Inserare · Inspectare · Gestionare · Vizualizare",
"ui.workspace_drawing_description": "Desene 2D cu vederi, cote și indicatoare",
"ui.workspace_footer": "Fiecare spațiu de lucru lucrează pe același proiect, istoric și selecție; se schimbă doar filele benzii. Comutatorul este blocat cât timp se editează o schiță sau o operație.",
"ui.workspace_layout": "Aspectul spațiului de lucru",
"ui.workspace_manufacture_description": "Feliere, trasee de sculă și configurare",
"ui.workspace_planned": "Planificat",
"ui.workspace_render_description": "Previzualizare produs, setări de scenă și imagini randate",

Some files were not shown because too many files have changed in this diff Show More