Compare commits

...

880 Commits

Author SHA1 Message Date
semmelrinde d7ad69e64f Pattern and Mirror: a hole placed by edge distances can be copied (#8055) (#8231)
A pattern or mirror replays its source hole's definition, so a hole with
reference edges finds its centre again from them. That happens after the
hole is drilled: through the part, the centre is then in the opening, and
the check that the centre lies on the face refused every copy. The replay
now skips that check; placing the hole itself still makes it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:16:51 +00:00
kaltenbrunnermarkus a0ec7dbdf0 Sheet metal: a cut leaves sheets it cannot reach untouched without a check (#10301)
Before a cut ran, every sheet metal body of its component went through a
boolean with the tool, to make sure the cut would not take material the
flat pattern cannot keep. For a sheet the tool does not reach, that check
only found nothing: in a design of 50 sheet parts it was about an eighth
of a hole's time. Now a sheet whose box lies clear of every tool's box is
left out of the check, with the same grown boxes the cut itself uses
(#10225), so a tool that touches or grazes a sheet is still checked.
Sheets shown in another frame and Intersect are checked as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:16:27 +00:00
github-actions[bot] 00f7e97578 Regenerate generated files and fold pending files [skip ci] 2026-11-02 17:11:23 +00:00
PaddleStroke 89f1234f28 Browser: placements start closed (#9274) (#9658)
* claim: #9274

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

* Browser: placements start closed (#9274)

A large assembly opened with every placement unfolded, so the browser was a
long list to scroll through. Placements now start closed; opening one shows
its own placements still closed, and closing and reopening it leaves them as
they were. Find in Browser, a pick in the view, Expand All and Collapse All
open and close them as before.

Preferences > Interface > Components start open (browser_components_open)
keeps every placement open from the start, as before.

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

* Browser: scenarios and the icon audit open placements, which now start closed

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

* Browser: tests that read rows inside placements set the open-placements preference

Three tests added on main since this branch started (joint folders, Features
folder marks, group rename) look at rows inside placements, which now start
closed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <tenbee91@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-11-02 16:39:25 +00:00
Colin Faulkingham 6b1eb5796e Sketch: preserve snapped line starts (#11181) (#11184)
* claim: #11181

* fix(ui): preserve line-chain snap starts (#11181)
2026-11-02 16:24:17 +00:00
bherbruck 927a5c3733 PR rules: a title has to make sense to someone who never saw the chat (#11202)
* PR rules: a title has to make sense to someone who never saw the chat

Agents write PR titles from inside their own context, or copy the issue's title, so a maintainer reading the list can't tell what a PR does. AGENTS.md, the contributing skill, CONTRIBUTING.md and the PR template now say who a title is for and show an unclear and a clear one.

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

* PR rules: write PR text in plain, literal English, and use a clearer example title

The first example still said "that window", the kind of pointing word the rule bans. The new rule takes the useful parts of Simplified Technical English without its approved-word list, which doesn't fit CAD terms.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:02:56 +00:00
bherbruck c7547eb3a8 Extrude: a face picked off the profile's plane is the target by default (#11201)
With a profile or face picked, clicking a face, vertex or parallel plane that is
not in the profile's plane now sets Extrude to To Object, not one more profile.
Clicking into the Profiles box first makes the next picks profiles again.
Preference extrude_auto_to_object (Tools) turns it off.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:00:40 +00:00
bherbruck 6cee7a91b9 Release 0.30.1 (#11176)
Bumps the version to 0.30.1 and folds the pending format changes into format 48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:10:02 -05:00
bherbruck 15e7615ce3 PR rules: a PR has to explain itself and show its picture (#11188)
A pull request now starts by saying what it is in plain words, and shows
anything there is to look at in its description. A file in the diff
doesn't count. The new Picture check fails a PR that adds to the showcase
or docs/screenshots without showing a picture. It isn't a required check.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:43:22 +00:00
bherbruck 70b2466965 Corner window tests: wait for the size preview before clicking OK or reading the error (#11180)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:57:52 +00:00
Vvynot ec1f78c6fa Hole: editing a Single hole replaces the picked face (#10692) (#10797)
The editor reused the new-hole Face input, which is multi, so a second pick
appended and OK silently kept the first. Editing now makes that input single,
so a pick replaces the place.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-02 14:45:37 +00:00
github-actions[bot] 01b592e0c8 Regenerate generated files and fold pending files [skip ci] 2026-11-02 14:52:32 +00:00
Ray f6e932f61a Rounded Rectangle: corners take parameter expressions, as a 2-point rectangle's do (#10956)
`sketch.rectangle.rounded` read `p0` and `p1` with `req_vec2`, so a corner typed as an
expression (`["W / 2", "H / 2"]`) was refused with "`p0` must be a point [x, y]", although the
2-point rectangle it hands the corners to already places expression corners and keeps them
following their parameters. The corners are now read with `corner_arg`, as the 2-point and
center rectangles read theirs; the worked-out values still check the radius against the sides.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:10:17 +00:00
Vvynot 8ab33bb80c Dimension: the value box keeps a 30 px gap from the pointer (#11057) (#11073)
A value typed before its target opened the box 18 px right and 10 px below the
pointer, so its frame covered the geometry a click was about to pick. Near the
viewport's right or bottom edge the clamp then slid the box back over the
pointer, losing the gap where it mattered most. It now keeps a 30 px gap, and at
an edge it opens on the pointer's other side instead of sliding back.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-02 14:08:54 +00:00
NOWN-DEV e33b8cfb07 Sheet metal convert: trays made in SolidCraft convert back (#7529) (#7580)
Convert to Sheet Metal refused a tray made with SolidCraft's own sheet metal, exported and
imported again: its walls run on past each bend's end (notch, tear and round corner reliefs), so
the edges beside the bend cylinder's edge never reach the other face's plane and the face could
not be trimmed ("a face beside a chamfer picked as a bend does not reach the face across it").
Where that edge is round or runs on along the bend's line, the face now keeps its corner and
steps down square to the other plane, a step the bend's setback takes up again; the inset drops
an edge the setback leaves shorter than the tolerance.

Test: trays_of_its_own_sheet_metal_convert_back (notch, tear and round, the walls made in one
flange feature, and notch and round with the short walls in a second one): the volume, the flat's
size and outline area and every bend's length are the source's.

Fixes #7529.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:04:39 +00:00
Dahak Builds 768dc253dd Fix mirror/pattern feature copy dropping bottom slice on hollow parts (#6452) (#7994)
* claim: #6452

* Engine: a pattern or mirror copy of a join keeps its ends as drawn (#6452)

The copy's tool is rebuilt against the current state, which already holds
the source feature's own material; the embedded-join-end trim (#2059)
read that material as the target already filling the copy's bottom band
and trimmed it away before the copy matrix was applied. Mirroring or
patterning a join on a hollow part silently lost the floor-level slice.
Copies now skip the trim: their ends are where the source drew them.

* chore: re-trigger duplicate PR check

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 14:03:53 +00:00
dudedagger42 1d98b59883 Sketch Break: a length becomes the distance between the old ends; no piece of no length (#6826) (#6834)
Breaking a dimensioned line left the length on the first piece, which the solver stretched
back to the whole length, skewing the line. A cut on (or a hair from) an end, as where a tangent
arc meets a line, made a zero-length piece with its coincident and collinear ties every time
Break was clicked there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:03:42 +00:00
Nathan Dane abea79d8ab Preferences redesign 4/11: General profiles, files and updates (#9876) (#10295)
* Redesign General preferences

* Update Preferences regression fixtures and gallery coverage

* Keep measured Preferences faces out of neutral exclusions

* Keep the profile name entry in Preferences search results

The redesigned Profiles section had no search-visible control, so a query
such as "profiles" dropped the whole section and the name field could not
be reached with a search active. Report the entry row as a hit so the
section stays on the results page, and point the typing-focus regression
test at the redesigned "Profile name" field and "Save as new" button. It
still checks that typed profile names stay out of the search box.

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-11-02 13:58:23 +00:00
Colin Faulkingham c4808e4715 MCP: clean up empty scratch folders on shutdown (#7836) (#7929)
* claim: #7836

* fix(mcp): remove empty scratch directories on command exit
2026-11-02 13:57:48 +00:00
Homero Thompson 6649cf4cfe Project: a refusal that names no body names the kind alone (#10626) (#10859)
The refusal of sketch.project ended in "face of " with nothing after the
"of" when the reference named no body: a picked face, edge or vertex is
found by its geometry, so the body it belongs to is not in the reference.

LinkSource::describe() now drops the "of <body>" when there is no body to
name and names the kind alone ("face", "edge", "vertex", "spun
profile", "isoparametric curve"). A source that names a body keeps its
name.

Only the text changes: the command refuses exactly what it refused before.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-11-02 13:49:26 +00:00
semmelrinde 9b431fea16 Hole: on a cone or torus face, drill along the face's normal, not a facet's (#10698) (#10885)
A hole placed on the side of a cone with no direction drilled along
the nearest display-mesh triangle's normal, up to 2.6° off the
surface's normal and sideways. Planes, cylinders and spheres already
gave their exact normal; cones and tori now do too, from the surface
the kernel reports for the face.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:48:20 +00:00
Steve Jason f484457c3b Expressions: sign(0) is 0, not 1 (#10996) 2026-11-02 13:47:34 +00:00
Colin Faulkingham 2339510e41 Sketch Trim: keep endpoint tangency on the surviving arc (#10851) (#10853)
* claim: #10851

* Sketch Trim: keep endpoint tangency on the surviving arc (#10851)
2026-11-02 13:47:26 +00:00
austin62jake a9fa61ce77 Viewport: the shader compiles in browsers again, so the web app's 3D view isn't black (#11080) (#11107)
* claim: #11080

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

* Viewport: the shader compiles in browsers again (#11080)

The ambient occlusion turn added in #9335 mixed `<<` and `|` without
parentheses. naga accepts that, so the desktop app ran, but browsers'
WGSL compiler refuses it: the sc_viewport module was invalid, every
pipeline built from it failed, and the web app drew nothing once a
design was open. The line gets its parentheses, and a test checks every
shader in the repository for the rule.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 13:46:45 +00:00
semmelrinde ac9d4e8a1d Move to Component: a sketch on a face inside the moved part goes along (#11063) (#11072)
* Move to Component: a sketch on a face inside the moved part goes along (#11063)

A face attachment stores the placement path it was found through. Moving
the placement into another component changes that path, the stored one
no longer starts at the design, and the move was refused as breaking
the sketch. Face attachments of sketches and construction planes, and
an emboss's face, now follow the move as the session's paths already do.

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

* Move to Component: attachments made through any placement of the old parent follow

A parent placed more than once is one definition: a face attachment made
through another placement of it than the one named also ran through the
moved placement and broke. Every stored path through the moved placement
now runs through its new place. A path that would grow past the 64
placements a design keeps is refused instead of saved unreadable.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:46:12 +00:00
Christian Glombek 39163148d9 Sheet metal: an edge flange on part of a stepped outline spans that part (#11034) (#11046)
A flange picked on one segment of a stepped base outline ran the base
face's whole length: trimming the picked edge back to the bend line
merged it with the outline's own edge on that line beyond the step, and
the flange took the merged edge. It now stays off the base's edges that
already lay on the bend line, so it spans the segment picked, with no
relief there: the base is already back at the bend line. The same
outline made by cutting notches into a rectangle gave the right flange
already.

Assisted-by: LLM
2026-11-02 13:45:56 +00:00
semmelrinde 3ca311f922 Sketch: a dog bone's outline outside it meets outside both notches (#10984) (#11000)
* Sketch: a dog bone's outline outside it meets outside both notches (#10984)

Where two tangent arcs touch at a cusp, the copies of the two arcs cross
twice, both crossings as far from the cusp, and rounding chose one: on
one side of a dog bone the outline ran both copies on and drew a loop
into the bone. Such a tie now takes the crossing that cuts both copies
short.

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

* Dog bone test: the outline goes outside, away from the picked bottom line

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:45:56 +00:00
Colin Faulkingham b978eeddcd Sketch: preserve short reference-line directions when dimensioning (#11001) (#11117)
* claim: #11001

* fix(sketch): keep short infinite references aligned
2026-11-02 13:45:36 +00:00
Vvynot 2ccca96929 Rename: a refused inline rename keeps the editor open (#10654) (#10952)
* Rename: a refused inline rename keeps the editor and says why (#10654)

commit_rename dropped the command's Result, so a refusal closed the editor
with no message and left the old name. It now returns the error, shows it in
the status line, and the inline editor stays open.

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

* Rename: keep the command's status line when the editor stays open (#10654)

The command completion already explains the refusal; do not overwrite it with
the raw engine error.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-02 13:44:34 +00:00
Vvynot 8ac4c03602 Extrude: going back reaches the view it was clicked in from an open sketch (#10571) (#11035)
Extrude started with the sketch still open ends the sketch on its way. With
"If a sketch ends: Return to the previous view", that end starts a camera
animation, and turn_for_extrude read the camera through settled_cam() - the
target of that animation - so the view saved as "before Extrude" was where the
sketch end was already turning, not where the command was clicked. The view is
now taken before the sketch is finished.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-02 13:44:01 +00:00
bherbruck 063f481ea0 API keys: read the base URL's host the way the HTTP client does (#10953)
The check that lets a key go only over HTTPS or to this computer ended the host part at `/` or
`?`, while the HTTP client also ends it at `#`, so the two could disagree about the host. It now
ends where the client ends it, and over plain HTTP the host part must be exactly this computer's
name with at most a valid port: no user name, backslash or stray character.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:18:51 +00:00
Colin Faulkingham 3c806394b0 Edit Sketch: restore timeline and undo state when finishing (#10691) (#10791)
* claim: #10691

* Fix sketch edit completion history (#10691)
2026-11-02 09:37:38 +00:00
Nathan Dane 3ddd73728b Render workspace: one editor per Browser selection; delete the Render studio panel (#6281) (#8000)
* Dispatch the Render inspector by selection on current editors

* Exercise native CPU capture through the Sampling editor
2026-11-02 09:08:23 +00:00
Dahak Builds ef939b52eb Parameters: one meaning for value in every reply; round reported numbers (#10747) (#10812)
* claim: #10747

* Parameters: value is mm/degrees in every reply; round reported numbers (#10747)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 08:26:24 +00:00
momysnow c1612e9234 Move / Copy with Copy on a component copies the component (#9914) (#9930)
* claim: #9914

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

* test: Move/Copy copy of a picked component (#9914)

Tests for a component picked in Move/Copy with Create Copy: it is copied as an
independent component placed with the move, the original stays, one undo removes
the copy; a body picked with Create Copy stays a body copy; a component picked
without Create Copy moves its bodies as before.

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

* fix: Move/Copy copies a picked component as a component (#9914)

With Create Copy ticked and the component pick mode on (or a Browser component
row), a picked component is copied as an independent component (Paste New,
component.paste_new) and the copy moves by the move's transform (occurrence.move
in the world). Bodies picked still copy as bodies; without Create Copy a component
still moves its bodies. The copy and the bodies are one undo step.

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

* fix: solid.move copies of components reply like a move and roll back (#9914)

A copying move with components replies as a solid.move does: the bodies' feature reply,
with the copies listed in occurrences. The bodies are mapped to their frame before any
copy is made. The solid.move help documents occurrences. A copy that fails part way
changes nothing and leaves no undo step.

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

* fix: Move/Copy takes a picked component whether or not Create Copy is ticked (#9914)

A component picked in the component pick mode, or a Browser component row, goes into
the Move bodies input as a component, so Create Copy can be ticked after picking it.
Without Create Copy the component still moves its bodies. Not while editing an
existing move. The Browser model tests read the bodies the command would move.

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

* fix: Move/Copy preview draws a copied component where the copy lands (#9914)

The fast preview took the component's bodies from the listed occurrences as well as the
bodies, drawn moved by the move, so a component copy previews at the moved place
instead of the scratch design's unplaced states. Test: a component-only copy and a
mixed copy preview where the copy lands.

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

* fix: a copying move refuses bodies that are not a list (#9914)

With components, a bodies value that is not a list was ignored; without components it
is refused. It is refused now, before anything is copied.

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

* test: an edited move never takes a component pick, through the click (#9914)

Clicks the component in component pick mode while an existing move is edited: the pick
stays a body. Fails when the editing condition is dropped.

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

* fix: Move/Copy preview takes components only for a copy, and the mixed test can fail (#9914)

The engine refuses `occurrences` without `copy`, so the preview leaves
such params to the full preview too. The mixed-copy test now gives the
component a taller box, so its height shows the component was drawn.

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

* docs: component_pick no longer needs Create Copy ticked (#9914)

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

* test: a component picked with its parent is copied once (#9914)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 07:56:22 +00:00
Yogesh D 544de55baa Localization: Russian "Close draft" reads as a draft analysis (#9284) (#9387)
* claim: #9284

* Localization: Russian "Close draft" reads as a draft analysis (#9284)

The report form's "Close draft" button said «Закрыть анализ уклонов»
(close draft analysis). It closes the report draft: «Закрыть черновик»,
the word the Russian catalog already uses for this draft.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 07:31:59 +00:00
semmelrinde 29a6bbdb43 Hole: say when a hole removes no material, or all of a body (#10701, #10702) (#10836)
* Hole: a hole moved off the material says it removes no material (#10701)

A hole whose position, parameter or sketch point moves it off the part
cut nothing and said nothing: the feature had no warning, and a
parameter sweep listed the value as fine. A hole whose tool is clear of
every body it cuts now warns "the hole removes no material" ("1 of 2
holes remove no material" for several), as a pattern does of its
copies. The check compares boxes only, from the meshes the hole already
found its face on, so a large hole pattern costs no extra booleans.

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

* Hole: a hole wider than the part says it removes all of it (#10702)

A through hole wider than the part removed the whole body with no
message, also when a parameter widened it later. The hole now warns
"the hole removes all of Body1" when a body it cuts is gone after the
cut.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 07:13:00 +00:00
github-actions[bot] a169ad4194 Regenerate generated files and fold pending files [skip ci] 2026-11-02 07:21:59 +00:00
semmelrinde d907078434 Section details card: every row shows, and the numbers read like the pill (#10708) (#10799)
The card's area is as tall as it was the frame before, and the first
frame laid the body out shorter than it ends up, so the scroll area
stayed short and the Hatch row (and Flip, for a tilted plane) was cut
off. The card may now grow to the height it is allowed.

The offset is shown to the design's length decimals and a tilted normal
to three places, as the pill shows them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 06:53:46 +00:00
kaltenbrunnermarkus 073bbc3f00 Assemblies: a construction axis follows its component when the component rotates (#10127) (#10212)
* claim: #10127

* Assemblies: a construction axis follows its component when the component rotates (#10127)

A construction axis or point built on a body now goes with that body when
Move/Copy moves or turns it. Each construction records the bodies its picks
lie on; the Move feature (not a copy) carries along every construction whose
bodies it all moved, with the same transform in the construction's component
frame. One that also reads something fixed stays where it is.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 06:40:58 +00:00
alfonsotames 9e5e143ac3 Sheet metal: a fold crosses slanted or curved edges of any panel (#10621) (#10922)
sheet.fold refused any fold whose bend zone wasn't a rectangle. It now
crosses the sheet's own outer edges, slanted or on any curve: the outline
is cut exactly across the zone, the panels keep their real outlines, and
flanges and other folds still go on them. The flat pattern's DXF keeps the
curve as drawn. A sharp corner in the zone stays refused.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:24:50 +00:00
Tarek 0dd8d9e5ec Add a dialog for Create Components from Bodies (#10919) 2026-11-02 05:23:00 +00:00
bherbruck 9fdf153054 Agent chat: only the chat's own tools skip the panel's question (#10955)
A permission question counted as one of SolidCraft's own tools when its title merely contained
"solidcraft". Now the whole title must be a tool of the chat's own MCP server, named as
`mcp__solidcraft-app__<tool>`, and its kind must be `other` or not given. Anything else goes through
the user's mode and remembered answers, or is asked in the panel.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:15:42 +00:00
Dahak Builds 5336007c2b Section Analysis: view.pick skips edges and vertices in the removed half (#10704) (#10712)
* claim: #10704

* Section Analysis: view.pick skips edges and vertices in the removed half

near_edges now applies the same section `kept` predicate the mouse picker
uses (viewport::pick, since #6722), so an agent picking through view.pick /
ui.pick no longer gets edges and corners the user cannot see.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 04:38:22 +00:00
bherbruck 21fc384e40 Control channel: a new connection has five seconds to show its token (#10961)
A client could connect and send nothing, or only blank lines, and hold one of the 16 connection
seats for as long as it liked. All reads before the token now share one five-second deadline; the
connection is then closed without a reply. An authenticated connection may idle as before.

`solidcraft-cli mcp --connect` used to open a connection at start and hold it until the first tool
call. It now only checks that the app is there, and the first call connects.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 04:23:23 +00:00
bherbruck 5d6a80ff35 Control token file: no file in a custom folder when icacls times out (#10958)
On Windows the token file is restricted with `icacls`, which may not finish in time (#6145). That
counted as success everywhere, and is safe in the default folder, the user's own local app data.
A folder picked with SOLIDCRAFT_CONTROL_DIR may be readable by others, so there a timeout now
removes the still empty file and returns an error. The default folder behaves as before. The
decision is a small function of its own, tested without running icacls.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 04:08:13 +00:00
bherbruck 9636188a0f Lock files: never written or read through a link (#10951)
A design's lock file went through the same writer as the design, which follows a symbolic link
so a linked design is saved where it lives. The lock now has a writer of its own that refuses
anything at its path but a regular file and replaces only that name. Reading a lock takes only a
regular file and no more than a lock's size. Saving a design through a link works as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:53:28 +00:00
github-actions[bot] ec39a8ebb4 Regenerate generated files and fold pending files [skip ci] 2026-11-02 06:12:24 +00:00
semmelrinde 450cc8b389 Russian: translate the 20 messages main gained after batch 7, construction lines included (batch 8) (#9445)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:32:48 +00:00
bherbruck 3c12c36bef Security policy: say what counts as a security issue and what's a normal bug (#10946)
Also points the links at tangency-labs/solidcraft. The old list asked for reports of file access outside the allowed folders, and for crashes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:29:43 +00:00
MarkusRe 6e67ae34b0 Agent chat: keep transcript shortcuts out of CAD (#10576)
Selectable transcript labels do not request TextEdit focus, so the global
clipboard handler also copied, cut and pasted selected CAD geometry while
the user worked with chat text.

Keep keyboard ownership with the visible chat after a pointer press until
the next press outside it. Release it when the panel is hidden, and do not
use selections from other native viewports as a global CAD shortcut guard.

Cover real label selection, clipboard events, fallback key events, quick
select-all, hidden panels and deliberate return to the CAD viewport.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:24:30 +00:00
SilentWolf75 1cbebe21bf Edit Hole: seed the face where the hole is now (#10689) (#10860)
* claim: #10689

Co-authored-by: Cursor <cursoragent@cursor.com>

* Edit Hole: seed the face where the hole is now (#10689)

Project the stored position onto the drilled face along the hole direction when opening the editor, matching evaluation, so OK works after a parameter moves the face.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 02:39:25 +00:00
github-actions[bot] 25a19cdefe Regenerate generated files and fold pending files [skip ci] 2026-11-02 05:40:52 +00:00
kaltenbrunnermarkus 12ebc80e15 Cosmetic tap markings: look for their cylinder only in nearby bodies (#10303)
* Holes and cuts: count the bodies a cut works on in the cost tests

A test helper, cut_work, adds a feature at the end of a design and reports
how many bodies its cut ran booleans on and which bodies it left exactly
as they were (same geometry, key and face names). A new test pins that a
cut still goes through the boolean where its tool touches a face, runs
along an edge, passes a hair beside a body or drills into a dome.

The sheet metal hole (#10226) and cosmetic thread (#10227) work reuse the
helper for their own shortcuts.

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

* Holes and cuts: leave bodies the tool cannot reach untouched without a full cut

A cut ran a whole boolean against every body of its component, and for a
body its tool does not reach that boolean only handed the body back: about
7 ms per body, so a hole in a design of 200 blocks took 1.3 s. Now a body
whose box lies clear of every tool's box is left as it is. Both boxes are
their meshes' grown past how far the exact faces can curve out from them,
the same test sheet metal cuts already use, so a tool that touches a body,
grazes it or drills into a dome still goes through the boolean. Bodies
shown in another frame, mesh bodies and Intersect (which removes the
bodies it misses) are unchanged.

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

* Cosmetic tap markings: look for their cylinder only in nearby bodies

A tap marking (a Thread feature, or the marking of a tapped hole) looked
for the cylinder at its point in every body of the design, and each
search meshes the body afresh: with many bodies and many markings that
grew quadratically (200 markings on 200 cylinders: 33 s of a 33 s run).
A body is now passed over when its box, from the mesh it keeps and
grown past that mesh's deflection, lies farther from the point than the
search itself would look (a twentieth of the body's size, plus its own
mesh's deflection). The search finds no face there, so the marking and
everything else stay the same.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:15:18 +00:00
dudedagger42 b470d46ffc Sketch: rebuilding a projection keeps the sketch's own curves on its points (#5388) (#5537)
* claim: #5388

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

* Sketch: rebuilding a projection keeps the sketch's own curves on its points (#5388)

When a projected face changed shape, update_link rebuilt the link with
drop_link_entities, whose remove_points also deleted every curve using a
projected point: a circle the user drew on a projected hole's centre vanished,
and the extrude on it lost its profile. Before the projected points go, the
sketch's own curves on them move to a free point at the same place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:09:59 +00:00
Vvynot 6e98bd474e Grid: a selected plane previews before Create Sketch (#10804) (#10900)
Selecting a plane in the view now draws the grid on it, so the plane a
sketch would use shows before the button is clicked. Deselecting returns
the grid to the ground.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-02 02:09:30 +00:00
Colin Faulkingham ad23882080 Assistant: recover when an agent ignores Stop (#10800) (#10916)
* claim: #10800

* fix: recover when ACP agents ignore Stop
2026-11-02 02:09:27 +00:00
ABDULLAH 63fb818f33 Arabic: profile label in the dialog selection list matches the curve and constraint labels (#10927) 2026-11-02 02:09:25 +00:00
dudedagger42 96509412e9 Extrude: a Cut To the far side of a curved wall takes the wall (#8634)
A Cut To Object on a curved face ran the tool through the target and kept
the tool less the body, the piece at the start. From outside a curved wall
to its inside face that piece is the air before the wall: the cut took
nothing, or the tool lay in the curved face and the boolean failed on the
coincident faces.

Where the target's material ends at the picked face under the whole
profile (rays over the profile cross it once, material before, none
after), the tool now ends flat halfway across the gap beyond the face. It
takes what lies before the face and meets no face of the body. Otherwise
the trim is as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:01:42 +00:00
froglemonade2 f2ffd8d233 Save As: refuse a name that opens as another kind of file (#9745) (#9752)
* Save As: refuse a name that opens as another kind of file (#9745)

file.save and file.save_as wrote a design container under any name, so a
design saved as part.step was refused when opened again and part.stl opened
as a broken mesh. A name doc.open reads as a STEP/CAD exchange file, a mesh,
a 2D drawing, a point cloud or a SolidCraft drawing is now refused, pointing
to .sc and, for export formats, to file.export. Names that open again as the
design (.sc, legacy extensions, .json, anything else) save as before.

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

* Save As: check the folder scope before the design name (#9745)

A path outside an agent's folders is refused for that first, whatever its
extension, as before; the design-name check runs after it in `write`. Fixes
solidcraft-mcp tests::tool_calls_stay_inside_the_agents_folders, which saves
`x.step` outside the agent's folders and expects the scope refusal.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 01:51:19 +00:00
SilentWolf75 2b08e0c6a7 AppImage: refresh the icon cache so the taskbar shows the mark (#10673) (#10858)
* claim: #10673

Co-authored-by: Cursor <cursoragent@cursor.com>

* AppImage: point the taskbar at the installed icon PNG (#10673)

Install hicolor icons before writing the desktop entry, refresh the icon cache, and set Icon= to the absolute 256×256 path so Cinnamon and similar panels show the mark instead of a thin strip.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 01:50:44 +00:00
kaltenbrunnermarkus 1885fa739b Assemblies: a second planar joint can be added (#10331) (#10371)
* claim: 10331

* Assemblies: a second planar joint turns its part a half turn to meet (#10331)

A second planar joint between two boards that mates faces pointing the
same way needs the board turned a half turn on the first joint. The loop
solve starts exactly turned away, finds no way round, and left the new
joint as a conflict; Move then said the part has no movable joint axis.

A new loop-closing joint now tries its own angle, each angle on the
joint path between its two parts, or one of each, a half turn round, as
the first joint of a chain moves its part into place, and keeps the
first that closes the loop without a new conflict. Faces that can never
meet stay a conflict and nothing moves.

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

* Move test: give the blocked part faces that can never meet (#10331)

The blocked-Move test from #10509 used two boards whose second planar
joint mates two faces pointing the same way. That joint now closes its
loop with the board turned a half turn, so the scenario no longer
conflicts. Mate the yellow front against the red top instead, which no
turn on the first joint can bring together.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 01:29:29 +00:00
github-actions[bot] cacfbbc578 Regenerate generated files and fold pending files [skip ci] 2026-11-02 05:03:27 +00:00
Joberndo 2ae13ba9cf Feature editor: Undo, Redo and the History bar wait while an editor is open (#10688) (#10786)
* claim: #10688

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

* Feature editor: Undo, Redo and the History bar cancel an open editor first (#10688)

Undo, Redo and History steps changed the design under an open feature
editor, whose OK and Cancel then refused it. They now cancel the editor
first, putting its marker back, as starting another tool already does.
While an editor is still opening they wait for it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:36:34 +00:00
raykholo e91f5d73bd Joints: a typed joint that closes a loop starts where its parts are (#6061) (#10336)
A relation given no values already started from where its parts sit
(relation_values), so the part moves no further than it must. A typed joint
(planar, revolute, cylindrical ...) added between parts another joint
already relates started its values at 0 instead, so the loop solver began
far from the current placement and could report a conflict for a joint that
agrees with how the parts are placed.

Seed every loop-closing joint, typed or relation, from the current
placement when no values are given. Joints in an open chain and joints with
explicit values keep their initial values as before.
2026-11-02 00:24:33 +00:00
github-actions[bot] da501178aa Regenerate generated files and fold pending files [skip ci] 2026-11-02 04:35:57 +00:00
Daniel Katz 3231f049d0 Insert Part: ASME/ANSI inch hex cap screws, hex nuts and SAE/USS washers (#10316) (#10335)
* claim: #10316

* Insert Part: ASME/ANSI inch hex cap screws, hex nuts and SAE/USS washers (#10316)

Part of #10316. New standard-part families, tabled in inches as the
standards give them and built in millimetres: hex cap screw (ASME
B18.2.1) and hex nut (ASME B18.2.2), 1/4 to 1 in UNC and UNF from the
thread catalogue, and plain washers SAE and USS (ANSI B18.22.1 Type A).
Cap screws take the standard inch lengths, 1/2 to 8 in, defaulting to
the first at or above 2.5 d.

Lengths read as fractional inches in Insert Part, Change Part Size, part
names and part numbers. Insert Part gains a Unit system list (Metric,
Inch) above Type; it opens on Metric. parts.library reports each
family's system.

Inch parts get no cosmetic thread: solid.thread takes only metric
designations (#9571), and the old path would have made a wrong metric
mark. 9/16 waits for the thread catalogue; split lock washers and
carriage bolts follow in later PRs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:21:21 +00:00
semmelrinde a22ea76ab6 Feature editor: a section, an analysis or the selection changed while it is open leaves OK and Cancel working (#10687) (#10822)
The editor accepts revision changes that only changed what is shown,
counted by the display revision, but only hiding and showing counted
there. Section Analysis, the surface analyses, continuity, the
selection, the status pill catching up and view.render_select bumped
the revision alone, so turning a section off with an editor open made
OK refuse ("the design changed while the feature editor was open") and
Cancel leave the timeline rolled back. They now go through
refresh_display, as visibility changes do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:18:00 +00:00
austin62jake a735620aa8 Circular pattern: the axis origin takes length expressions and follows them (#10852) (#10855)
* claim: #10852

* Circular pattern: the axis origin takes length expressions and follows them (#10852)

solid.pattern.circular with axis {origin, dir} kept its origin as plain
numbers, so a pattern about a parameter-driven point stayed where the
point was when the pattern was made: the seed moved with the parameter
and the copies did not. Expressions were refused (#248).

The pattern's origin is now a point of expressions, as a Mirror plane's
origin is: numbers or length expressions, evaluated on each rebuild, and
renamed with their parameters. Plain numbers are saved as numbers, as
before. timeline.edit sets pattern.origin as expressions, and editing the
pattern in its dialog keeps them. One pending format change.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:17:15 +00:00
rugbymauri 059119b345 solid.move: an inserted part moves in world space beside another body (#9660) (#9683)
* claim: #9660

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Move bodies of differently turned components in the world together (#9660)

A Move feature moves each of its bodies in the body's own frame by one
vector and one turn. `solid.move` mapped the world vector and axis into
the frame of the first body listed only, so a body in a frame turned
otherwise (a part inserted with a non-default direction, moved together
with a block of the root) moved along its own axes.

When the bodies listed lie in frames that turn differently (or, for a
turn, also sit in different places), the move is now split into one
Move feature per frame, each with the vector and axis mapped into that
frame, in one undo step. Bodies sharing a frame still make one feature.

Fixes #9660

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-11-02 00:16:56 +00:00
jeromehzl e016c40f43 Sketch Intersect: picked faces meet as their whole surface, not the span under the pick (#9009) (#9107)
A surface extruded from a fit-point spline is one B-rep face per span,
joined by hidden seams; the viewport picks and highlights it as one face
(Mesh::surface_patch). sketch.intersection_curve turned a face pick into
the single nearest face, so the curve stopped at that span's ends.

project3d::mesh_of now returns the picked face's surface patch and
mesh_intersection keeps the triangles of every face in it. Body sources
and Project to Surface are unchanged.
2026-11-02 00:16:06 +00:00
austin62jake 1fb9dadcd9 Language: a Traditional Chinese system opens in Traditional Chinese, macOS's zh-Hant-TW included (#10306) (#10307)
* claim: #10306

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

* Language: a Chinese system tag goes by its script, then its region (#10306)

macOS lists its languages with a script (zh-Hant-TW), which no catalog
matches exactly, so the base zh (Simplified) was picked for a Traditional
Chinese system. Hong Kong and Macao tags got Simplified the same way.

A Chinese tag now picks zh-TW for the Hant script, zh for Hans, and
without a script zh-TW for the TW, HK and MO regions, zh otherwise.
Other languages keep the exact tag, then the base language.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:15:53 +00:00
github-actions[bot] 91a19548d3 Regenerate generated files and fold pending files [skip ci] 2026-11-02 03:47:23 +00:00
kaltenbrunnermarkus a277839236 Section Analysis: tilting the plane rebuilds the outline index once, not on every drag frame (#10373)
* claim: sectiontilt

* Section Analysis: tilting the plane no longer rebuilds the outline index every frame (#10373)

The section outline cut through an index of slabs along one normal, rebuilt
whenever the normal changed: tilting a Section Analysis rebuilt it every
drag frame (about 0.15 s on a 2.5 million triangle part). The index is now a
grid of cells holding each triangle by where its box sits, with the box
around each cell's triangles, so it serves every normal. It finds the same
segments, in the same order and to the bit, as cutting every triangle.

The viewport keeps one index per shown body mesh, no longer per normal.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:15:29 +00:00
cronos b5d931ff4c Preserve offset distances when trimming their source line (#10391) 2026-11-02 00:15:27 +00:00
Christian Glombek d7a17de3d9 Sheet metal: a perforation field bounded by a sketch's outline (#732) (#10754)
`sheet.perforate` takes an optional `sketch` (and `profiles`): the field's
outline. Holes go only inside its closed profiles, out of their inner loops,
`margin` clear of its edges, on a grid from the outline's lower-left corner.
On a folded sheet the outline maps through each panel the sketch lies on and
is clipped to it, so it never reaches a flange in the flat; a sketch on no
face of the sheet is refused. A mirrored copy mirrors the outline, and an
edit that names no sketch (the Perforate dialog has no outline) keeps it.
Without a sketch the field covers the whole sheet as before.

The feature saves `sketch` and `profiles`, a sheet's perforation its outline
in flat coordinates: one pending format note, no migration.

Assisted-by: LLM
2026-11-02 00:14:41 +00:00
leValdemar19 a33619fd2b Tangent Plane: line it up with a reference plane, at an angle (#8822) (#9002)
* claim: #8822

* Tangent Plane: line it up with a reference plane, at an angle (#8822)

`construct.plane.tangent` takes an optional `reference` (a plane or planar
face) and `angle`. On a cylinder, cone or torus the plane touches where the
face turns towards the reference, turned by the angle about the face's axis,
on the side of the pick; on a sphere it is parallel to the reference. The
plane follows the reference when it moves. The Tangent Plane dialog gets a
Reference plane input and, once one is picked, an Angle field; editing the
plane brings both back. Saved as a pending format change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:13:22 +00:00
austin62jake e4e3c8609c OBJ export: welded as STL and 3MF are, so a curved body opens again closed (#9074) (#9213)
* claim: #9074

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

* OBJ export: welded as STL and 3MF are, so a curved body opens again closed (#9074)

The OBJ writer joined vertices only when their coordinates were equal bit
for bit, but each face of a curved body writes its edge points again a few
ulps apart, so neighbouring faces shared no vertices and the file reopened
as an open mesh with no volume.

It now welds each body as the STL and 3MF exports do (threemf::weld_tagged,
with the same tolerance), and every corner keeps its normal: the normal of
the original corner it came from, or for a point a side was split at, the
normal interpolated across the triangle.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:13:06 +00:00
CamWight 325917f2ae Ambient occlusion: no artifacts on the preview or on hollow bodies (#9327) (#9335)
* Ambient occlusion: the radius follows what the frame draws (#9327)

In an empty design the radius came from the model's box, which is empty, so a
preview got the smallest radius and the depth map's precision showed as specks.
It now comes from the box the depth range uses, and with nothing to draw there
is no occlusion.

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

* Ambient occlusion: the term drawn once and blurred by depth (#9327)

The lit pass worked the term out for every fragment it shaded, under a pattern
that changed from pixel to pixel, which read as grain. The term now has its own
texture, drawn once for each pixel of the prepass, with a fixed 4 x 4 pattern, and
a 4 x 4 blur that keeps to each pixel's plane takes the pattern out without
crossing an edge. The lit pass reads the result. The prepass and the term share
their pipelines and draw walk.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:12:57 +00:00
austin62jake 5ad2503503 Timeline folders: editing a feature in a folder leaves no stale id, and such designs open again (#9373) (#9393)
* claim: #9373

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

* Timeline folders: editing a feature in a folder leaves no stale id, and such designs open again (#9373)

timeline.redefine runs the edited feature's command on a scratch copy with the marker just after
the feature, and takes the new definition from the feature that run makes. Inside a timeline
folder that trial feature joined the folder too; it was removed from the feature list but not
from the folder, and its id, handed back, was the next feature's. Each edit left that id in the
folder (the same one, edit after edit), naming no feature or, once a later feature took it, a
feature twice, and the reader then refused the design: "damaged design: invalid feature in a
timeline folder". The trial feature now leaves the folders as it leaves the feature list.

The reader drops a folder entry naming a feature or joint the design doesn't have, or one a
folder already holds, and opens the design: folders only group the timeline, so nothing else is
lost. A folder damaged in itself is still refused.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:12:50 +00:00
PapaEcho f43198988d fix(ui): keep profile-name typing out of Preferences search (#9505) (#9568)
Report focused keyboard ownership from the shared text editor, matching the other Preferences fields. Regress real Name-field typing, focus retention, unchanged search text, and saving the resulting profile.
2026-11-02 00:12:36 +00:00
Sebastian Höpfl 6f73bd95c4 Assemblies: dragging a part with mirrored copies no longer meshes them on every frame (#10101)
A mirror of placed bodies follows its source, so each frame of a drag evaluated
it and made new copies with empty mesh cells. A reflection can't share its
source's triangles, so every copy was tessellated from nothing, and so was
anything an evaluated feature after the mirror made.

Keep the first copy of a body with its source: each later copy is a proper rigid
move of it, so a world state draws them from one set of triangles. The model
keeps each copy's own lazy mesh. A feature evaluated again that gives the same
solids (an import) keeps its meshes.

A mirror of placed bodies no longer searches its copies' revolved faces when no
later feature can build on bodies. That depends on the design alone and is part
of the mirror's placement digest.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:11:45 +00:00
CamWight 9d7544929e Tests: the same language on every PC (#10297) (#10298)
Since #10183 the app follows the system's language, and the tests followed it too, so a test
naming an English word failed on a PC set to another language. CI's runners read as English
and never saw it. In tests the system's language is now English: `cfg!(test)` for this crate's
own tests, as the engine's folders do, and the harness for the tests outside it. The preference
stays `system`, and the app is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:11:16 +00:00
bherbruck 3125a79827 Release 0.30.0 (#10894)
Bump the workspace to 0.30.0 and fold the pending design format changes into format 47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:55:11 -05:00
kaltenbrunnermarkus eb0cecee31 Web app: the offline check with the real app finishes its download (#10368) (#10370)
* claim: 10368

* Web offline test: each copy of a real build gets a file of its own (#10368)

With --site, the six builds were copies of one real build that differed
only in index.html. The overlapping-downloads step holds back the files
the next build lacks, so nothing of v5 was held: its download finished
before v6 launched, v6's worker then retired and deleted it (correctly,
no page used it), and the step waited 240 s to see it complete.

Successive deployments differ in more file names than that (Trunk names
files after their contents). Each copy now gets own-<build>.txt, like
the stand-in's build-named script, so v5's own file is held as the step
intends.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:10:42 +00:00
semmelrinde 27951f40c3 Parameters: the Filter box also reads expressions (#9706) (#10385)
Typing a parameter's name now lists every parameter whose expression
uses it, and a value finds the parameter that holds it. The filter
still reads names, comments and groups, and a text parameter's text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:10:09 +00:00
Homero Thompson 9e3cf05c4f fix(sketch): a line of another sketch projects from the Project dialog (#10390)
A straight line of another sketch, picked into the Project dialog, came
in as a named sketch curve, which the projection preselection did not
know: it refused with "select geometry or bodies that can be projected"
although the OK button was live. The same reference the external pick
stands for is now made for it, so the line projects (#10291).

Co-authored-by: vdirienzo <vdirienzo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-11-02 00:10:01 +00:00
Homero Thompson 6459c9fcf0 A context error is not something to report (#10181) (#10594)
* wip: the diagnosis of #10181 (the bug reproduces on current main)

sketch.project has .enabled(in_sketch) (crates/engine/src/cmd/sketch_project.rs:24)
and in_sketch (crates/engine/src/cmd/mod.rs:285) answers 'no sketch is being edited'.
Pressing P outside a sketch goes keymap::press -> sketch_tool_for
(crates/ui-egui/src/keymap.rs:451-457) -> app.start("sketch.create") with
d.then_start = Some("sketch.project"): the sketch IS created, but the Project
dialog opens and the command is refused, so the dialog shows the red row and the
'Create Issue...' button, which opens the Report a bug form over the viewport.

The probe here reproduces the bug on the CURRENT main (two variants). It is a
diagnostic probe, not the regression test: it must be replaced before a PR.

* test: a context error offers no Create Issue button (#10181)

The Project panel showed the red row for a CONTEXT error ('sketch.project is not
available right now: no sketch is being edited') and offered the 'Create Issue...'
button, which opens the Report a bug form over the viewport. A context error is
not something to report, so the panel must not offer it.

RED: the test fails with the exact message of the issue.

* fix: a context error is not something to report (#10181)

The Project panel offered "Create Issue..." for the error the engine
raises when the command is not available in the current context
("`sketch.project` is not available right now: no sketch is being
edited (Create Sketch or Edit Sketch first)"). That refusal is guidance
about the context, not a failure of the design, so there is nothing to
report: ending the sketch under an open Project panel should not invite
the user to file a bug.

error_rows already spared the missing-pick errors ("select ...", "pick
...") the same way. It now spares the context refusals too, using the
engine's own kind rather than the message text: CommandSpec gains
disabled_refusal, which is the engine's own Disabled refusal for the
command in the current session, and the UI matches the dialog's error
against it. A real failure of the command (an invalid face, say) still
shows the red row with "Create Issue...".

* style: rustfmt the test of #10181

The formatter joins the two asserts of the test onto one line (the
repository sets max_width = 150 and use_small_heuristics = Max). The
tokens of the file are unchanged: the diff is the line breaks of the
asserts and nothing else.
2026-11-02 00:09:40 +00:00
austin62jake 5e7f3651fb Angle fields: a decimal point typed into an angle stays (#10582) (#10631)
* Angle fields: a decimal point typed into an angle stays (#10582)

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

* Angle fields: a decimal point typed into an angle stays (#10582)

Edit Canvas applies its Angle box at each keystroke and keeps the angle
in radians, so the box gets back a hair off the value it set; egui's
DragValue takes that for a change made elsewhere and rewrites the text
being typed, dropping a decimal point just typed (-3.7 became -37).
angle_field now hands the DragValue the value it set while the value it
is given is within 1e-9 of it.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:09:32 +00:00
Colin Faulkingham da586bc048 Fix viewport Unisolate after isolating a component (#9894) (#10635)
* claim: #9894

* Fix viewport recovery after component isolation
2026-11-02 00:09:23 +00:00
austin62jake 062a7b8288 Tangent Arc: a first click off a curve's end is refused at once, saying where to click (#10653) (#10655)
* Tangent Arc: a first click off a curve's end is refused at once, saying where to click (#10653)

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

* Tangent Arc: a first click off a curve's end is refused at once, saying where to click (#10653)

The Tangent Arc tool took any first click as its start; a click on a
line between its ends snaps to a place that is no point yet (on:l3:...,
mid:l1), and the second click then failed with "unknown sketch point"
naming that internal reference.

The tool now refuses such a first click at once, with its own hint as
the error, asking the command's rule (tangent_arc_starts_at, factored
out of arc_tangent: the picked point or one at the same place, and a
drawn curve leaving it). The command refuses a snapped start with its
existing message that start must be the end point of a line or arc.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:09:17 +00:00
Colin Faulkingham 5bbfa5ea6c Sketch: preserve source geometry when deleting a pattern (#10651) (#10661)
* claim: #10651

* fix(sketch): preserve source geometry when deleting patterns
2026-11-02 00:09:10 +00:00
austin62jake 75c46dad7b Tests: the autosave scenario crashes once its autosave has landed (#10690) (#10694)
* Tests: the autosave scenario crashes once its autosave has landed (#10690)

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

* Tests: the autosave scenario crashes once its autosave has landed (#10690)

s25_autosave_recovery 'crashed' right after the extrude; under load the
autosave of it, written on a worker thread, had not landed, so the next
launch recovered the earlier autosave without the body (step 16).

The harness's crash restart now runs frames, bounded, until the active
autosave has caught up (Autosaver::caught_up: no write running and the
last autosave holds the design as it is). The app's own ticks still do
the writing, so an autosave that stopped writing still fails the test.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:09:03 +00:00
github-actions[bot] af54b09bb1 Regenerate generated files and fold pending files [skip ci] 2026-11-02 01:51:55 +00:00
github-actions[bot] dcbca523a0 Regenerate generated files and fold pending files [skip ci] 2026-11-02 01:42:49 +00:00
froglemonade2 e9b08f4af4 Control channel: report the dialog as data, the open tool windows, and what ui.start opened (#9087) (#10740)
* claim: #9087

* Control channel: report the dialog as data, the open windows, and what ui.start opened (#9087)

`ui.inspect.dialog` (and `ui.editFeature`'s reply) was the dialog's Rust Debug text, which
changed with every refactor and couldn't be parsed. It is now an object: the dialog's command,
title, the feature an edit rebuilds, its selection inputs, its error, and the commands OK runs
(or what it waits for). `hover` in `ui.inspect` and `ui.selection` is a selection-like item
instead of Debug text.

`ui.inspect` gains `windows`: the tool windows open beside the model (Parameters, Bill of
Materials, Design Properties, Sketch Report, material card review) by the command that opens
each, and the Help menu's (About, Updates, Shortcuts, Crash Reports, Report, Messages) by their
`ui.help` item. Before, an agent couldn't tell these windows were open.

`ui.start` and `ui.editFeature` reply with what they opened (tool, dialog, windows), the status
line, and `pending` when the start waits for the history and opens on a later frame.

Scenario `dialog` checks match words of the dialog's state from the app directly, so the
scenario files are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:08:48 +00:00
Dahak Builds 6a7275a5af Agent chat: copy an entire sent prompt or assistant message (#10675) (#10741)
* claim: #10675

* agent chat: Copy message on each sent prompt and assistant text block (#10675)

A visible Copy message button on each copyable transcript message and a
right-click menu with the same action. Assistant replies copy their
original Markdown, prompts copy the text as entered; picture-only prompts
and thoughts get no action. Hover-only message areas keep partial text
selection working.

* agent chat: complete Copy message locales and tests (#10675)

* agent chat: route the message copy menu through ui_kit::Container (ui-containers gate)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 00:08:41 +00:00
PapaEcho 666356dba0 Assistant: configurable show/hide shortcut (#10764) (#10767)
* claim: #10764

* Assistant: make show/hide shortcut configurable (#10764)
2026-11-02 00:08:23 +00:00
austin62jake 3fe2b3386f Sketch: Remove Spline Point takes one point out of a spline (#10783) (#10793)
* Sketch: Remove Spline Point takes one fit point out of a spline (#10783)

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

* Sketch: Remove Spline Point takes one point out of a spline (#10783)

sketch.spline.remove_point takes one fit or control point out of a
spline. The spline keeps its id and runs through its other points, which
stay where they are. The point goes with the constraints on it, unless
another curve uses it. A closed spline's closing point, an end joined to
other geometry, the last 2 points (3 when closed), and projected, offset
or patterned splines are refused with a message.

The right-click menu of a spline point, picked alone, offers it beside
Reverse Spline Direction.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-02 00:08:11 +00:00
Colin Faulkingham afe06a1a3d Edit Joint: show components for point and frame origins (#7749) (#10838)
* claim: #7749

* fix(ui): show components for point and frame joint origins
2026-11-02 00:07:29 +00:00
Colin Faulkingham eaf34d2528 Construction planes: refuse duplicate feature names (#10707) (#10743)
* claim: #10707

* Fix construction plane name collisions (#10707)
2026-11-02 00:06:59 +00:00
Dahak Builds 2b9261050f Sketch: stop tying expression-placed points to the origin with an automatic Coincident (#10703) (#10722)
* claim: #10703

* Sketch: do not tie expression-placed points to the origin with an automatic Coincident

A point whose coordinates are parameter expressions follows those
expressions. When they came to 0 at draw time the point landed on the
origin and connect_points tied it there with a Coincident, exactly like
a snapped click; the next parameter change then conflicted with the tie.
connect_points now skips points that carry exprs; plain-number points
keep the snap.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 00:06:51 +00:00
semmelrinde eac1425661 Hole: a face name shared by two bodies is no split face (#10699) (#10872)
* Face names: one name on two bodies is no split face (#10699)

A hole placed on the top of one of two bodies an extrude made, or on a
piece of a split body, warned "face `F2:top` was split; using the piece
nearest the picked point": the face name is on both bodies, and any
name found more than once counted as split. A name on several bodies,
one face on each, now finds the face the point lies on without the
note; pieces on one body, or a point on none of them, still say split.

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

* Hole: look for a face name shared by two bodies on the body the point is on (#10699)

The lookup over every body counts a name on several bodies as split on
purpose (a copy's face is not a piece of the source's, see
`the_same_face_on_a_copy_is_not_a_piece_of_the_named_bodys_face`), so
the first change, which made it unique there, is undone. A hole whose
face name is found split while its point lies on one of those faces now
looks again on that body alone: one face of the name there is the face,
whole; pieces on that body still say split.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:06:43 +00:00
github-actions[bot] 88dbda42a7 Regenerate generated files and fold pending files [skip ci] 2026-11-02 00:31:13 +00:00
Homero Thompson 9e30ff5878 fix(#5505): a zero length dimension keeps constraining the sketch (#10790)
The residual of a length dimension is the magnitude of the distance between
the line's two ends. At zero the magnitude has no derivative, so the rank
analysis read the constraint as if it were not there and the sketch reported
a degree of freedom that was not there.

At zero the two ends of the line meet, so the residual says it with the
vector between them, the way a coincidence does. That is the treatment a
distance of zero already gets (upstream #4397); the length family was left
out of it.

Regression test: crates/sketch/src/tests.rs, zero_length_keeps_the_line_constrained.

Co-authored-by: Homero Thompson <homero@local>
2026-11-02 00:05:09 +00:00
thocsmith 4c4c768d57 Cancel button on the Computing toast (#8266) (#10366)
* claim: #8266

* Computing toast: a Cancel that gives up on the move (#8266)

The Computing toast for a history command (marker move, undo, redo,
suppress, reorder) or a delete's check computing off the window thread now
has a Cancel button. It drops the job and the commands queued behind it;
the design stays as it was, since a deferred command only runs for real
once its evaluation is in. A feature edit applying keeps its dialog's own
Cancel, so its toast shows none.

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

---------

Co-authored-by: thocsmith <193096300+thocsmith@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:04:53 +00:00
Dahak Builds 83fe1f874f Sketch Project: keep hidden origin planes hidden in Project (#10778) (#10834)
* claim: #10778

* Sketch Project: keep hidden origin planes hidden (#10778)

WIP from dead worker: fix + test written, gates not yet run.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 00:04:45 +00:00
Colin Faulkingham 0734616104 View cube: follow the selected projection (#10634) (#10640)
* claim: #10634

* Make navigation cube follow viewport projection (#10634)
2026-11-02 00:04:43 +00:00
Dahak Builds d68d86dae9 Dialog form cards widen their label column for the current language (#10620) (#10646)
* claim: #10620

* widen dialog form-card label columns to the measured label width (#10620)

* fix compile of the grow tests

* grow the card only past the label cap and tag the form scope in place (#10620)

* keep scope_grow crate-internal so the gallery scan stays element-only (#10620)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-02 00:04:41 +00:00
rugbymauri 7d86a8cfd3 Sketch: dragging a constrained centre point of a centre rectangle moves it, not resizes it (#10510) (#10535)
* claim: #10510

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Sketch: dragging a centre rectangle's constrained centre point keeps the rectangle's size

A centre rectangle's centre sits on the middle of its diagonal, and a
press there picked the diagonal's midpoint before the centre point. The
drag then took the diagonal by its middle: with the centre free the
rectangle moved along, but with the centre held (on the origin, on a
line) the diagonal turned and stretched about it, and the rectangle
grew or shrank with the pointer, many times its size.

A drag that starts on a line's middle where a point is held by a
Midpoint constraint now drags that point, as a drag that starts on the
point does. A centre held on the origin stays there and the rectangle
keeps its size; a centre on a line slides the whole rectangle along it;
a free centre still moves the rectangle. Clicking the middle still
selects the line's midpoint for constraints.

Fixes #10510

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-11-02 00:04:38 +00:00
semmelrinde cea07cbc66 Sketch Slice: cut at the sketch plane while a Section Analysis is on (#10705) (#10768)
With a section on, Slice in the Sketch Palette lit up but the model was
still cut only by the section: the viewport took the sketch's plane only
when there was no section. Slice now takes it whenever it is on in the
sketch being edited; the section comes back when Slice is turned off or
the sketch closes. A slice is hatched as it is without a section.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 00:02:59 +00:00
Colin Faulkingham 0b90731096 Dialogs: readable sketch selection labels (#10752) (#10770)
* claim: #10752

* Fix readable sketch selection labels in dialogs (#10752)
2026-11-02 00:02:37 +00:00
SilentWolf75 afa4f72f7d Insert SVG: say why a sketch is too large (#10874) (#10880)
* claim: #10874

Co-authored-by: Cursor <cursoragent@cursor.com>

* Insert drawing: name the sketch size cap when a file is too dense (#10874)

A large SVG/DXF failed with opaque \"sketch is too large\". Say the 20000 point/curve limit and that scale alone does not reduce entity count, and refuse dense drawings before adding them.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 23:37:55 +00:00
Sebastian Höpfl cfc46448a8 Joints: an edge origin shares the joint faces' measurement (#10866)
`face_of` called `Body::faces`, which tessellates the whole body, on every
solve. Face origins went through the cached `ModelBody::joint_faces`, at
the same tolerance, but edge origins never did, so one edge origin on a
large body cost a drag or a Drive Joints step far more than the solve.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:51:42 +00:00
github-actions[bot] 58c64ca4d4 Regenerate generated files and fold pending files [skip ci] 2026-11-01 22:29:27 +00:00
Nathan Dane 5c7e50cb67 Preferences redesign 2/11: shared widgets and painters in ui_kit (#9874) (#9890)
* claim: #9874

* Add shared Preferences cards, figure painters and gallery entries to ui_kit

* Preferences: Arabic, Hebrew and Vietnamese carousel labels

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

* Use the app's selection colours in the Preferences cards

* Measure Preferences figure inks and use shared palette metrics

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:58:35 +00:00
Colin Faulkingham b6aabb88ff Insert Design: place reused links from their parent origin (#10813) (#10825)
* claim: #10813

* fix: place reused linked designs from parent origin (#10813)
2026-11-01 21:47:53 +00:00
DuBearte a1dc849e8d Sketch Fillet and Chamfer: a refused size's error goes once the size fits (#9316) (#9469)
A corner dialog copied the preview's error into its own, where nothing
cleared it when the preview recovered. The dialog already shows the
preview's error, so the copy is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:25:21 +00:00
Kushagra Kumar 446590dca1 fix(engine): report design variant in timeline.inspect and accept in timeline.add (#10748) (#10792)
* fix(engine): report design variant in timeline.inspect and accept in timeline.add (#10748)

* ci: refresh checks to clear cancelled workflow run
2026-11-01 21:00:44 +00:00
CamWight 303236e524 Tests: io's byte mutator moves to a testkit crate, a dev-dependency only (#5184) (#7682)
* Tests: io's byte mutator moves to a testkit crate, a dev-dependency only (no behaviour change)

`solidcraft-testkit` (the Testkit class xtask/src/layers.rs reserves) holds the mutator and the
no-panic check unchanged: the same seeds, mutations, budget and doc comment. io's reader fuzz
tests use it as a dev-dependency and give the same case counts; the mutator's own test moves
with it. The co-editing wire's decoders will fuzz with it too (#5184, CE6).

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

* Cargo.lock: testkit at the workspace version

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-11-01 20:28:58 +00:00
bherbruck 97336ccfe7 Kernel pause: pull requests are judged by the kernel directories they change (#10807)
A pull request outside crates/kernel, crates/geom and vendor/truck* is no longer labelled for a
kernel word in its title. Issues and discussions keep the title check.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:19:41 +00:00
cronos e1d4cb433c Cover sketch line-arc fillets in both selection orders (#10242) 2026-11-01 20:19:16 +00:00
Thodoris Tsagkogeorgas 0ad74f2577 Revolve: keep the angle grab point when the sweep is zero (#9601) (#9602)
A revolve of 0° still builds no solid. The handle stays on the profile so the angle can be dragged back into range.
2026-11-01 20:18:27 +00:00
semmelrinde 835e261593 Coil: up and down arrows on the Revolutions field (#7481) (#7766)
* claim: #7481

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

* Coil: step arrows on the Revolutions field (#7481)

The Coil dialog drew Revolutions as a plain expression row, the one
count without the step arrows the pattern quantities have. It is now a
count row from 1 to 200 turns (the helix's limit); a fraction still
types in, and an arrow steps it to the next whole turn.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:12:56 +00:00
github-actions[bot] 84c3b6356c Regenerate generated files and fold pending files [skip ci] 2026-11-01 20:42:12 +00:00
github-actions[bot] d730a273dc Regenerate generated files and fold pending files [skip ci] 2026-11-01 20:33:03 +00:00
semmelrinde 89cb651084 Warnings: quote a long generated name by its start and end (#8673)
A chamfer's warning quoted the full generated names of fourteen split edges,
122,701 characters (#8541). Names over about 80 characters are now quoted by
their first 48 and last 24 characters.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:12:42 +00:00
Kai Evans 9efcb461f8 Feature edits: OK computes off the window thread in a dialog without a live preview too (#7388)
Offset Plane and a cosmetic Thread have no live preview, so their edits' OK ran on the window
thread and recomputed every later feature there. Any edit now goes through the off-thread
transaction. The editor's pinned original is used only while the dialog previews, as
open_feature_edit keeps it: a Thread switched from Modeled to cosmetic edits the design as it is,
so a body hidden meanwhile doesn't refuse it as a changed design.
2026-11-01 20:11:05 +00:00
Daniel 958ab867cd Press Pull on a chamfer or fillet face edits it where you clicked (#4986) (#9771)
* claim: #4986

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

* Press Pull on a chamfer's face changes the chamfer's distance (#4986)

A chamfer's face is flat, so solid.press_pull and the Press Pull start
path in the UI took it for a planar face and extruded it. Both now look
for a fillet's or chamfer's face before asking whether the face is
planar, so a chamfer's face reopens the chamfer and its distance grows
by the distance pulled, as a fillet's radius already does. A first pick
with Press Pull already open is routed the same way.

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

* Press Pull on a blend face puts the arrow on the edge clicked

A fillet or chamfer reopened by Press Pull from one of its faces drew
its radius or distance arrow on the feature's first stored edge, which
can be a corner the user did not click and cannot see (the Sample
Plate's fillet, from the far corner). The click's point now goes with
the reopened dialog as a hint that is never saved, and the arrow sits
on the feature's edge nearest it, at the point on that edge nearest the
click. Opened any other way (the history, Edit Feature) the arrow stays
on the first edge as before.

A click point that is not finite is not kept, and a point on a flat face next to a chamfer still extrudes that face (tested).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:11:00 +00:00
Jesper We cffa6e1580 Loft and Surface Loft: two sections from one sketch (#8221) (#8222)
* claim: #8221

* Loft and Surface Loft: two sections from one sketch (#8221)
2026-11-01 20:10:13 +00:00
semmelrinde ce296a1ad5 Extrude preview: a round face sweeps round walls, with lines up its corners only (#8838)
The live preview of extruding a picked face built its walls from the
face's display triangles and drew a line up the wall at every boundary
point, each wall piece shaded flat. A round face (the Bolted Flange's
flange) looked like a polygon while extruding. Lines now go up only
where an edge of the face ends and the outline turns there more than
its curves do, and a wall's normal turns with it between smooth pieces.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:09:21 +00:00
semmelrinde 0515dee9ce Colour popover: a held step button keeps stepping, and R, G and B have tooltips (#10201) (#10203)
* Colour popover: a held step button keeps stepping, and R, G and B have tooltips (#10201)

The step buttons beside each colour number sensed clicks only. egui
stops counting a press as a possible click once it outlasts its longest
click (0.8 s), so the held button stopped repeating after 8 steps. They
now sense drags too, which keeps a long press down on the button until
it is let go.

R, G and B now carry tooltips (Red, Green, Blue) as H, S and V already
did: a single letter doesn't name the colour in every language.

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

* Colour popover: the R, G and B tooltips in Vietnamese too, a complete catalog (#10201)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:08:43 +00:00
Vvynot 0478f7e401 Hole: reference Distance shows the document precision (#10693) (#10777)
* Hole: reference Distance shows the document precision (#10693)

A measured reference distance showed the raw float (10.000000868569572 mm).
The field now reads at the document's length precision while Refs keeps the
measured value for the model, so OK still sends what was measured.

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

* Hole: restored literals use the document precision (#10693)

Round every literal reference distance on Edit Feature, including comma
decimals and exact whole values; expressions stay as written.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 20:05:49 +00:00
Rec0iL 52f0b52b45 Agent chat: a searchable model picker with a custom entry, listed from every provider, and OpenClaw and OpenRouter presets (#2582) (#10255)
* ui_kit: search_picker, a filtered dropdown with a custom entry

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

* Agent chat: endpoints list their models with names, say when listing fails, and OpenClaw and OpenRouter presets (#2582)

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

* Agent chat: a typed model goes to endpoints and to agents that offer a model option

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

* Agent chat: the header's model pickers search and take a custom model

The popup can't grow once it is open, so search_picker now fixes the height of what changes below
its box when it opens: the custom row or a late list could otherwise end up out of reach.

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

* AI preferences: pick an endpoint's model when adding it and on its card (#2582)

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

* Agent chat: model picker rows read from the left, and a refused typed model says which

From running it: list_picker centres its text, which reads badly for a long list of ids, and an
agent's own refusal can say no more than "Internal error". The search box also asks for a repaint
while it waits for the focus, so an idle window does not stop short of it.

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

* Agent chat: docs for the model picker

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

* Agent chat: an endpoint's answer that is no model list says so, an empty list too, and editing lists with the kept key

A 200 without a `data` array (a web page where the endpoint should be, as OpenClaw's gateway
serves until its chat endpoint is enabled) was cached as a successful, empty listing, so the
picker stayed blank with nothing to say and never asked again. Now it is an error with its own
note, an empty list says so, and both are asked for again the next time.

The Edit dialog lists its endpoint's models when it opens, with the kept key read from the
keychain on the listing's thread, as the card does; the key stays out of the field.

The method-not-found test expects the refusal notice that names the model, as every refused
model gets since this branch.

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

* Agent chat: models from several providers fold under their providers inside the picker

As Chemiseblanc does in #10187: with two or more groups, `search_picker` draws each group as a
header with a chevron, the name and how many of its choices are listed, and a click folds or opens
it. Before anything is typed only the current value's group (and the one the caller asks for) is
open; a group with a match opens once something is typed; a fold made by hand stays until the popup
closes. Enter takes the first row shown, so never one folded away. A lone group keeps its plain
heading.

The header's model picker feeds its providers in as groups, so the separate provider drop-down
goes; the control channel's `provider` now says which group opens first. A header click gives the
focus back to the search box, so typing goes on filtering.

The dialog's listing test checks what kind of listing it got rather than translated words, which
the listing thread may render in another language than the test's.

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

* Agent chat: rustfmt the provider groups

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

* Agent chat: an endpoint's models named `Vendor: Model` fold under their vendors

OpenRouter lists `openai/gpt-6.1-terra` as "OpenAI: GPT-6.1 Terra". A listing whose ids read
`vendor/model` and whose names read `Vendor: Model` now names its providers, so the picker folds
it under its vendors like a multi-provider agent; the rows read the model's name, and the id stays
in the tooltip and in the search. An agent's own `Provider: …` word still comes first. One vendor
is one flat list, as before.

The Add dialog's picker has no current model to prefer, so with nothing to prefer every group
starts open; folding is a click away.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:01:26 +00:00
ABDULLAH 1ae93218a7 Arabic: outline term in the cut preview tint help (#10801) 2026-11-01 19:50:21 +00:00
P'tite Tête 3017641b41 fix: Projection d'une silhouette à congés coniques fermée en un profil (#10773)
- snap_tangent_arcs : chaque extrémité d'arc prend la ligne la plus proche
- une ligne ne prend qu'une extrémité d'un arc ; l'autre passe à sa ligne suivante
- une ligne plus loin sur le contour est ignorée quand l'extrémité touche déjà une autre pièce
- tests : 4 unitaires (project_order.rs) et the_silhouette_of_conic_rounds_closes

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-11-01 19:41:32 +00:00
github-actions[bot] 17b9a8e67f Regenerate generated files and fold pending files [skip ci] 2026-11-01 19:55:56 +00:00
github-actions[bot] 90c09eaaf9 Regenerate generated files and fold pending files [skip ci] 2026-11-01 19:48:16 +00:00
Dahak Builds 251573a5a4 MCP: compare the model with a reference picture (#1486) (#6416)
* claim: #1486

* Compare picture: overlap and edge offsets of the model against a reference picture (#1486)

inspect.compare_picture lays the model's silhouette over the part found in a
PNG or JPEG and slides it to the best overlap: overlap (0..1), the model's
edges against the picture's in mm at nine stations, and an overlay PNG. The
MCP tool compare_picture runs it and hands the overlay back as an image block.

inspect.silhouette grows a reusable silhouette::outline for the model side.

* chore: re-trigger checks (defaults line already present in PR body)

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

* i18n: carry the Compare Picture label in Arabic, Hebrew and Vietnamese too

The complete-catalog guard (every_complete_catalog_carries_every_english_message)
failed because the pending file covered only en/ja/zh; ar, he and vi are
complete catalogs and need the key as well.

* Compare picture: one uniform scale, capped fit, sturdier part finding

Review on #6416: fit no longer stretches the axes apart (width is the default,
both scales uniformly by width and reports aspect_mismatch); the fitted model
takes the same MAX_GRID cap as the picture; the border flood seeds only from
border pixels near the border's main colour, reads alpha, retries at a
stricter step, and says which case failed; the slide searches the whole
placement range instead of a tenth around the centre; the model's holes get
the picture's fill rule; crop becomes crop_px / crop_fraction with explicit
units. Pins #1486's bracket (overlap 0.865, bottom -10 mm at the upright) and
adds a regression test per problem.

* Compare picture: alpha only for a see-through border, part_fills_crop, outline-only wording

Review re-test on #6416: one translucent backdrop pixel no longer switches the read to
alpha — alpha decides only when most of the border is see-through, and any ink is then
part, so a part drawn at alpha 100 survives; part_fills_crop: true takes the crop box as
the part itself, for one view cropped tight off a board where the part takes most of the
border and the flood would keep the backdrop strip instead; a third backdrop pass compares
each pixel to the border's main colour, so a white part on (245, 245, 245) JPEG at quality
75 separates; the command, tool and docs descriptions now say the compare is outline only:
holes are not compared. A banded light backdrop stays a follow-up. A test per case.

* MCP compare: format the picture tests

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

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-11-01 18:30:18 +00:00
Nathan Dane bdce595fb3 Render results: retained renders, Live and render compare, render all shots (#6281) (#7375)
* Render results: keep up to eight completed renders and select or discard them

* Render results: Live and render compare with a divider, and render all shots

* fix: use comparison theme tint and reveal reset test targets

* fix: restore Unicode expectations in render workspace tests
2026-11-01 18:30:11 +00:00
semmelrinde 862807ffc9 Lost GPU: the per-frame probe meets a D3D12 device reset before the first upload does (#6528) (#7767)
* claim: #6528

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

* Lost GPU: the per-frame probe opens a command list, as the upload would (#6528)

On a reset D3D12 device the empty submission before a frame's uploads
went through (it only signals a fence), and the first queue write then
panicked inside wgpu-core (PendingWrites::activate unwrapping Lost).
The probe now submits a command buffer with a debug marker: finishing it
makes the same begin_encoding call, through a path where wgpu loses the
device and runs its lost callback instead of panicking. The picture
capturer's probe does the same.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:20:40 +00:00
github-actions[bot] 6f368a5b77 Regenerate generated files and fold pending files [skip ci] 2026-11-01 19:01:13 +00:00
rbousigues88 a0ed5a9923 Measure: a fillet's torus or a sphere reports its radius (#7159) (#7160)
* claim: #7159

* Measure: a fillet's torus or a sphere reports its radius (#7159)

The face of a fillet round a circular edge is a torus and a ball's face a sphere, which
Measure listed by area only. Their face item now carries radius, diameter and surface
(and the ring radius of a torus); they stay out of Shape, so distances are unchanged.

Fixes #7159.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 18:03:17 +00:00
N3MI-DG 38ae728538 Preview: new surface is tinted whatever its size, and the tints can be turned off (#9111) (#9112)
* claim: #9111

* Preview: new surface is tinted whatever its size, and the tints can be turned off (#9111)

A preview decided which faces of a changed body were new surface by asking
whether their triangle centroids lay on the old body, within a tolerance that
follows the part's size. A round or chamfer small beside the part never leaves
that tolerance, so it was drawn as if it had always been there, and sampling at
a fixed step told alike faces apart by nothing but how many triangles they had.

The kernel already records which faces an operation generated, so `build` takes
that record alongside the geometric test. The test stays because a body carries
the history of whatever last made it, which is not the previewed operation when
a parameter change re-evaluates the design: the record only ever adds faces to
the answer. The vote itself now asks about every centroid of a small face,
spreads a large face's samples without a resonant step, and calls a face new
above an eighth of its samples, which a blend tangent on both of its sides
reaches with the one row of facets in five that is clearly off the old surface.

Graphics gains two settings, both on as they come, so nothing changes out of
the box: Tint new surface in a preview draws a changed body as itself when it
is off, working out no face at all, and Show removed material in red leaves the
see-through cut and the ghost of the shape a feature replaces fully
transparent, drawing only their outlines.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-11-01 18:02:57 +00:00
github-actions[bot] 147d1cfadd Regenerate generated files and fold pending files [skip ci] 2026-11-01 18:35:09 +00:00
github-actions[bot] d135e1659c Regenerate generated files and fold pending files [skip ci] 2026-11-01 18:26:58 +00:00
froglemonade2 3ccf04d4b2 Edit Extrude: close on the first Escape (#9084) (#9757)
* claim: #9084

* Edit Extrude: close on the first Escape (#9084)

An edit dialog whose value names a parameter (`T` in Edit Extrude) opened with
that field focused and its completion list showing the longer names (`tan(`),
unarmed. The list took the first Escape, so the dialog needed two.

An unarmed list now leaves Escape to the window until the box is typed in, as
it already leaves Enter and Tab; once typed in, Escape dismisses the list first
as before. Its hint drops "Esc dismiss" in that state.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:02:39 +00:00
N3MI-DG 2014e60ae6 Sketch Offset: an Instances count stacks offsets one behind another (#9827) (#9828)
* claim: #9827

* Sketch Offset: an Instances count stacks offsets one behind another (#9827)

Offset made one offset at a time, so a set of evenly spaced outlines meant
running it again on the curves it had just made, once per ring: a separate undo
step and a separate driving dimension each time.

`sketch.offset` now takes `instances`, a whole number or a unitless expression
from 1 to 100 (1, today's behaviour, by default). The per-group work the command
did inline becomes `offset_rung`, and `offset_ladder` runs it once per instance,
each rung offsetting the one before it the same distance again, so instance n
stands n distances from the curves picked. Every rung of a side is driven by
that side's own dimension, so editing it moves the whole set and keeps the
spacing even, and each side of a Two Sides or Symmetric offset grows its own
ladder. A rung the shape has no room for refuses the whole command and names
which instance it was.

A later rung reads its side from the geometry, through the rung's own
normal-distance annotation, rather than from the distance's sign: an output's
stored ends need not run the way the chain through them is walked, and a
reversed walk reads the same sign as the other side. A circle is the exception
and keeps the sign, because it offsets concentrically with no chain to walk, and
a step past a ring smaller than the distance lands beyond the centre, where the
circle's "outside the current radius" side test reads as the outward side: an
inward ladder turned round and grew again (1, 4, 1, 4 mm) instead of running out
of room.

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

* Sketch Offset: the Instances row in the offset window (#9827)

The offset window gets an Instances row under Distance: a whole number with step
arrows, 1 by default, so the count can be walked up a ring at a time and the
preview follows each step. The count is not kept in the design, as Mirror's and
Scale's values are not, so the window sends the number it evaluates to rather
than the text.

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

* UI kit: a count's step arrows are not text (#9827)

The arrows stand inside the editor, which asks for the text cursor across its
whole rect, so hovering one showed an I-beam and the arrows read as part of the
number. They take the ordinary cursor back after it, and the pointer is read
from the arrow's rect rather than from the hover the editor underneath competes
for, which keeps the hover highlight in step with it.

The pattern Quantity rows share this field and had the same thing.

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

* Sketch Offset: Instances label in the complete catalogs and the other shipped languages

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

* Sketch Offset Instances: translations for the complete catalogs

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-11-01 18:02:32 +00:00
Nathan Dane c454c05f88 Preferences redesign 1/11: design reference in docs (#9873) (#9889)
* claim: #9873

* Add the Preferences design reference: pages, figures, measurements, tokens

* Fix the selection look in the design reference to match the app
2026-11-01 18:02:24 +00:00
merik 1cc90036fb File: a command for each export format, so each can have a key (#10176) (#10177)
File › Export STEP…, STL…, 3MF…, OBJ… and URDF… were UI actions around
the shared file.export, so Keyboard Shortcuts had no row for them.
file.export_step, _stl, _3mf, _obj and _urdf now do what those items do
when started from a key, command search or the menu (a save picker; Save
As Mesh for STL and 3MF), and take a path when called with JSON. The File
menu shows their keys. Each has a contract and a reply type
(schema/file.rs).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:02:16 +00:00
Dahak Builds 176c22acaa MCP: explain when an unknown command id is actually an MCP tool name (#10678) (#10680)
* claim: #10678

* MCP: explain when an unknown command id is actually an MCP tool name

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 15:36:04 +00:00
ABDULLAH 2ab5dedfc1 Arabic: pan term in the Creo navigation help (#10667) 2026-11-01 15:13:48 +00:00
Dahak Builds e77c55549f Sketch: a value box must not swallow a click on the reference line's endpoint (#10600) (#10639)
* claim: #10600

* Sketch: a value box must not swallow a click that lands on a sketch point (#10600)

The construction line tools' Length box waits beside the pointer. When the
pointer reaches the reference line's own endpoint it can land under that box
(the box is anchored to the tool hover, which freezes while the pointer rests
on the box), and the box's text edit takes the click, so the tool never sees
it. A release over the tool's own value box or construction/normal switch that
lands on a snapped sketch point now goes to the tool; a click on the box away
from any sketch point still edits the box.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 13:31:14 +00:00
github-actions[bot] e7bb038795 Regenerate generated files and fold pending files [skip ci] 2026-11-01 13:27:53 +00:00
Marcel Karger a5a561caae Add Creo Parametric navigation preset (#3507) (#9322)
* claim: #3507

Co-authored-by: Codex <codex@openai.com>

* Add Creo Parametric navigation preset (#3507)

Add middle-button orbit, Shift+middle pan and Ctrl+middle vertical drag
zoom, with downward dragging zooming in. Keep the default navigation
preset and existing presets unchanged.

Refs #3507

Co-authored-by: Codex <codex@openai.com>

* Navigation: Creo preset needs the NX preset's reverse_drag_zoom field

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

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:57:15 +00:00
Dahak Builds 462de5b9d2 Fix extrude preview placement in placed components (#2103) (#10596)
* claim: #2103

* Pin #2103: extrude-cut preview placement on placed component occurrences

* Format #2103 regression tests

* Satisfy clippy lints in #2103 regression tests

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 12:46:17 +00:00
kaltenbrunnermarkus be425c6102 Assemblies: use lookup maps when building the world state (#10497)
* claim: Assemblies: use lookup maps when building the world state

* Assemblies: use lookup maps when building the world state

world_state_reusing, made again after every geometry command in a design
with components, filtered all bodies for every placement, searched the
timeline for every sketch and construction of a placed part, and searched
all bodies twice per tap and placement. It now builds the feature to
component map, the bodies of each component (in body order) and each
tap's component and body offset once per call. The result and its order
are unchanged.

On a 100-part assembly placed 5 times (800 sketches, 200 taps) one world
state drops from 124 ms to 9.5 ms (release, median).

A test-only counter of searches through all features or all bodies
checks that the work no longer grows with the sketches and taps.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:39:41 +00:00
Colin Faulkingham bc4d35b097 Avoid rebuilding bodies when rename updates feature references (#6287) (#10588)
* claim: #6287

* fix: reuse body geometry when renaming references (#6287)
2026-11-01 12:15:47 +00:00
Colin Faulkingham 78c5b8de42 Fix fastener placement on transformed washers (#9648) (#10583)
* claim: #9648

* Fix fastener picks on transformed washers
2026-11-01 12:13:41 +00:00
kaltenbrunnermarkus 9738861d61 Sketch drag: the crash test cleans up its folder like its neighbours (#10602)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:13:19 +00:00
ABDULLAH 0cf4da606c Arabic: one term for parameters in the equation curve and text help and the joint move hint (#10604) 2026-11-01 12:09:56 +00:00
github-actions[bot] 074cc758ee Regenerate generated files and fold pending files [skip ci] 2026-11-01 12:12:00 +00:00
kaltenbrunnermarkus 47e8b96c2f Placement digests look up bodies by name once (#10492)
A join, cut or intersect without explicit targets reads where every body
of its component is placed. Its placement digest searched the model for
each of those bodies by name, then the timeline for its component and the
placements for that component's frame: O(B^2 + B*F) per feature, on every
evaluation, also when every feature comes from the cache.

The bodies, their components (Document::model_body_components) and each
component's frame are now looked up once per digest. The bodies are still
hashed in name order with the same values, so every digest stays the same
bit for bit and saved caches stay valid.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:45:22 +00:00
Dahak Builds 7b3d84fb33 Per-component History hiding (#10393) (#10578)
* claim: #10393

* Per-component History hiding: flag, commands, Browser menu, bar toggle (#10393)

* Carry the new History-hiding strings in the complete catalogs (#10393)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 11:42:33 +00:00
github-actions[bot] 59a40aabc0 Regenerate generated files and fold pending files [skip ci] 2026-11-01 11:43:09 +00:00
kaltenbrunnermarkus b557f22f97 Sketch: a construction/normal switch in the construction line tools (#10258) (#10551)
* Sketch: a construction line with a length can be a normal line (#10258)

`construction: false` on a construction line command that has a
`length` makes the segment a normal sketch line, ready to close a
profile, instead of construction geometry. The default stays
construction, and an infinite line is always construction.

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

* Sketch: a length box in the construction line tools (#10258)

The horizontal, vertical, parallel and perpendicular construction line
tools show a Length box beside the pointer from the start. It stays
empty (shown as infinite) and leaves the keyboard to the tool's
shortcuts until a digit is typed; then the box takes the keyboard.
Without a length the tools work as before: one click, an infinite line.
With a length the first click sets the start and a second click the side
the segment runs to, sent as the command's `length` and `near`, so the
segment gets its length dimension. The angle tool has a Length box
beside its Angle box. A typed length keeps the start's snap, and the
preview draws the segment the click will create.

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

* Sketch: a construction/normal switch in the construction line tools (#10258)

A Construction | Normal switch sits under the Length box of the
construction line tools. Normal sends `construction: false`, so a
segment with a length becomes a normal sketch line, ready to close a
profile, without converting it afterwards. The switch starts at
Construction with each start of a tool. An infinite line is always
construction, so without a length the switch is greyed out and its
tooltip says why. It belongs to the tool alone: the Sketch Palette's
drawing line type (X) is left as it is.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:15:21 +00:00
github-actions[bot] 29acaf8642 Regenerate generated files and fold pending files [skip ci] 2026-11-01 10:44:57 +00:00
kaltenbrunnermarkus 36bdf4ea3c Sketch drag: drop the second document copy per frame (#10507)
* claim: Sketch drag: drop the second document copy per frame

* Sketch drag: drop the second document copy per frame

edit_solved copied the document to edit it, then stored it with
`*s.doc_mut() = doc`. The command's undo snapshot still holds the old
document, so doc_mut copied the whole design a second time only to
overwrite it. Store the edited document with Session::set_doc instead,
which forgets the same caches and puts it in a new Arc. The design lands
at a new address as before and the command's end records it for the
crash journal (#7502).

A live drag frame on a design of 200 sketches (20k curves) drops from
19.2 ms to 9.4 ms; 50 sketches: 2.3 ms to 1.7 ms.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:13:17 +00:00
kaltenbrunnermarkus ddd65cc4bb Move: say a part's joint conflicts instead of 'no movable joint axis' (#10509)
* claim: Move: say a part's joint conflicts instead of 'no movable joint axis'

* Move: say a part's joint conflicts instead of 'no movable joint axis'

When a joint of the selected part conflicts with the others (the solver
can't close its loop), Move found no axis and said "This part has no
movable joint axis.", pointing at joint types and limits. It now names
the conflicting joint and suggests editing, suppressing or deleting it.
Parts with no free joint value and no conflict keep the old message.

Refs #10331

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:10:01 +00:00
kaltenbrunnermarkus 2a410c2d76 Sketch: Intersect can pick sketch lines (#10484) (#10512)
* claim: #10484

* Sketch: Intersect can pick sketch lines and model edges (#10484)

The Intersect dialog only took faces or whole bodies, so a click on a
line of another sketch (the sides of a rectangle crossed by a plane
through its middle) picked nothing, although sketch.intersect already
cuts edges and sketch curves with the sketch plane. The pick list now
takes faces, edges and other sketches' curves, and the selection filter
reads Geometry | Bodies like Project's.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:09:55 +00:00
ABDULLAH 01b43999a5 Arabic: unambiguous last sentence in the Plane Through Axis And Point help (#10561) 2026-11-01 10:09:49 +00:00
Dahak Builds bcb5efcaa4 Right-to-left: multi-word Hebrew/Arabic names read backwards; prefs search misses multi-word labels (#10471) (#10562)
* claim: #10471

* Show given names in bidi display order; find RTL prefs labels typed in reading order (#10471)

crate::bidi ran over catalog text only, so a multi-word Hebrew or Arabic
body name showed its words left to right in stored order, and Preferences
search compared display-order labels against reading-order queries.
given_name now returns right-to-left names in display order (browser rows,
tabs, status bar all draw names through it), and text_matches also scores
the query after the same bidi transform.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 10:09:44 +00:00
semmelrinde 74ee047d3f Configuration rules: Then says it lists only the table's columns, and how to add one (#10532) (#10544)
* Configuration rules: Then says it lists only the table's columns, and how to add one (#10532)

A rule drives or restricts a column of the table only, so Then leaves out a
feature without a Suppress column or a parameter without a column, where
When lists them all. A note under the list now says so and points at Add
Column.

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

* Configuration rules: the two notes in the complete catalogs (Arabic, Hebrew, Japanese, Vietnamese, Chinese)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:09:38 +00:00
kaltenbrunnermarkus ab6e6941c7 View: look hidden bodies up in a set (#10496)
* claim: View: work out consumed sketches and hidden bodies once per revision

* View: work out the sketches features use once per design

Each sketch_shown call walked every feature of the design to tell whether
the sketch is used, and the viewport asks about every sketch several
times a frame. The used sketches are now gathered into a set once per
design (keyed on the design itself, so any edit reads them again).

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

* View: look hidden bodies up in a set

body_visible walked the hidden-bodies list for every body asked about, and
the viewport asks about every body several times a frame. The hidden
bodies now join the hidden joints and folders in the view's visibility
index, built when the list changes.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:59:42 +00:00
github-actions[bot] a93d7b207c Regenerate generated files and fold pending files [skip ci] 2026-11-01 10:14:19 +00:00
kaltenbrunnermarkus 2b65164159 View: work out the sketches features use once per design (#10545)
* claim: View: work out consumed sketches and hidden bodies once per revision

* View: work out the sketches features use once per design

Each sketch_shown call walked every feature of the design to tell whether
the sketch is used, and the viewport asks about every sketch several
times a frame. The used sketches are now gathered into a set once per
design (keyed on the design itself, so any edit reads them again).

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:53:47 +00:00
kaltenbrunnermarkus fbb00dc7b2 Canvas: store reference image data as shared text so edits stop copying it (#10489)
* claim: Canvas: store reference image data as shared text so edits stop copying it

* Canvas: store reference image data as shared text so edits stop copying it

Every edit clones the document, because the undo history still holds the
old one, and the canvas image was a plain String: each of the up to 200
undo states owned its own copy of the base64 text. Canvas::data is now
Arc<str>, like the shared STEP text, meshes and point clouds; serde's rc
feature writes it as the same JSON string, so design files stay byte for
byte the same.

A 4.2 MB image with 250 edits: peak memory 1.09 GiB -> 42 MB, run time
1.16 s -> 0.12 s.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:46:17 +00:00
semmelrinde 7702316039 Bosnian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10445)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:44:28 +00:00
semmelrinde bb0b9a1d2f Browser: a folder that holds only a new group shows it (#10526) (#10529)
* Browser: a folder that holds only a new group shows it (#10526)

New Group from the root's context menu made an empty group in the Bodies
folder, but the Browser draws no folder without items, so in a design
with no bodies yet nothing appeared. A folder that holds groups is now
drawn with them, even before anything is in it.

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

* Browser: test that a new group in an empty folder shows and takes a dragged body (#10526)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:44:17 +00:00
kaltenbrunnermarkus 74eda80b5d Extrude dialog: work out the automatic operation once, not every frame (#10491)
* claim: Extrude dialog: work out the automatic operation once, not every frame

* Extrude dialog: work out the automatic operation once, not every frame

An open Extrude window with the automatic operation on asked the engine
for `auto_operation` every frame. That probes the bodies with
`Mesh::contains`, rays through every triangle, so a large part cost tens
of milliseconds per frame with nothing changing.

The window now keeps the answer with a key over what it depends on: the
extrude's parameters (participants included), the design's revision and
id, and the active component and occurrence path. The latch still runs
every frame as before.

Open Extrude over a 1M-triangle mesh body: 29.8 -> 0.87 ms per frame
(headless harness, median of 3).

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:43:06 +00:00
kaltenbrunnermarkus ea63eec845 Extrude dialog: keep the taper limits while the profile is unchanged (#10503)
* claim: Extrude dialog: keep the taper limits while the profile is unchanged

* Extrude dialog: keep the taper limits while the profile is unchanged

The taper handle is drawn every frame while the extrude window is open,
and every frame it worked out the inradius of each picked profile and of
each of its holes: a grid search over every side, about 200 ms a frame
for a plate with 64 round holes. Only the height changes between frames.

The radii are now kept in the window's state with the profiles they were
worked out for, and worked out again only when the profiles differ. The
limits come from the same arithmetic, so they are unchanged.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:37:47 +00:00
HiroKenEz 64a55392d8 Sketch: a Line chain survives Undo (#10016) (#10019)
* claim: #10016

* Sketch: a Line chain survives Undo (#10016)

Undo could take away the segment the chain was drawing on from, and every
further click was then refused with "unknown sketch point". The chain now
remembers where each segment started and goes back to the last one still there.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:19:23 +00:00
github-actions[bot] 3a6920d10f Regenerate generated files and fold pending files [skip ci] 2026-11-01 09:39:05 +00:00
kaltenbrunnermarkus 228b311a6f Raytracer: map faces to looks once (#10502)
* claim: Raytracer: map faces to looks once

* Raytracer: map faces to looks once per body

Preparing a body for the CPU raytracer walked the body's face look list
one entry per work step for every triangle, so a body cost T x (F + 1)
steps (each through the job host) instead of T. The face looks are now
sorted once per body, keeping the first entry per face as before, and
each triangle finds its look with one binary search. Pixels are
unchanged; with 10 000 faces and 50 to 500 face looks, preparation
drops by 40 to 88 %.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:16:23 +00:00
kaltenbrunnermarkus 16f00d4cf9 Opening: validate the document once when a model cache is present (#10501)
* claim: Opening: validate the document once when a model cache is present

* Opening: validate the document once when a model cache is present

Opening a design that keeps its model parsed and validated document.json
twice: reading the model cache checked it before decoding the cached
bodies, and read_document, which follows straight after, runs the same
migrations and validation and fails the open when they fail. The open
path now reads the model without that check; geometry(),
read_design_geometry and the preview keep it. A broken document fails the
open with the same error as before.

3,000-feature design with its model cache, bench-open parse step:
426 -> 293 ms (median of 8, heavy machine load). Saving the opened
design gives a byte-identical file.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:16:12 +00:00
Dahak Builds 9fa3c85c68 Invert the curved ViewCube roll arrows (option) (#10483) (#10539)
* claim: #10483

* ViewCube: an option to invert the curved roll arrows (#10483)

A new Navigation preference, "Invert the curved roll arrows"
(invert_curved_roll_arrows, off by default): with it on, the two curved
arrows at the cube's corner roll the view the other way within the
screen plane - the clockwise arrow rolls counterclockwise and the
counterclockwise arrow rolls clockwise. The sibling of the cardinal-arrow
inversion from #10286, which left these arrows alone.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 09:13:00 +00:00
github-actions[bot] cda0277c3e Regenerate generated files and fold pending files [skip ci] 2026-11-01 08:49:46 +00:00
kaltenbrunnermarkus f233f000e8 Mesh imports: compare by pointer before comparing contents (#10490)
* claim: Mesh imports: compare by pointer before comparing contents

* Mesh imports: compare by pointer before comparing contents

MeshData derived PartialEq, so the dirty check and the autosave check
walked every coordinate and index of a scanned mesh after each edit,
although the document and its saved copy share the same arrays. Compare
shared arrays by pointer first and value by value otherwise, as Points
and Colors already do. About 1.9 ms per comparison for a mesh of one
million triangles goes to nothing.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:17:11 +00:00
kaltenbrunnermarkus fcb0a925e2 Sketch: hash sets for the stale-pattern check before each solve (#10493)
* claim: Sketch: use a lookup map for curves in pattern upkeep

* Sketch: use a lookup map for curves in pattern upkeep

Pattern upkeep runs before every solve, each drag frame included, and
looked up every copy curve and its arc points with a linear search over
all ids. Dragging another line in a sketch with a 500-copy slot pattern
spent about two thirds of each frame there. The ids now resolve through
maps built once per call; the result is the same to the bit.

The solve bench gains a circular slot pattern section.

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

* Sketch: hash sets for the stale-pattern check before each solve

The check whether a pattern lost a seed, copy or relation built three
ordered sets of all ids before every solve. Only membership is asked, so
hash sets give the same answer; a drag frame in a sketch with a 500-copy
pattern takes another third less.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:17:05 +00:00
kaltenbrunnermarkus 71624cf7fa Sketch: use a lookup map for curves in pattern upkeep (#10524)
* claim: Sketch: use a lookup map for curves in pattern upkeep

* Sketch: use a lookup map for curves in pattern upkeep

Pattern upkeep runs before every solve, each drag frame included, and
looked up every copy curve and its arc points with a linear search over
all ids. Dragging another line in a sketch with a 500-copy slot pattern
spent about two thirds of each frame there. The ids now resolve through
maps built once per call; the result is the same to the bit.

The solve bench gains a circular slot pattern section.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:16:59 +00:00
PapaEcho ce72abacf9 Construct: plane through an axis and a point (#10178) (#10472)
* claim: #10178

* feat: add plane through axis and point (#10178)
2026-11-01 09:15:25 +00:00
Dahak Builds f25a8e6abc Fix tooltip delay bypass on sequential menu hovers (#10481) (#10482)
* claim: #10481

* Restart tooltip delay per menu row and place tips beside the drop-down (#10481)

* Format

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-11-01 09:10:54 +00:00
Leo 24b91bd453 Sketch Concentric: accept a circle or arc and a sketch point (#10487) 2026-11-01 07:57:43 +00:00
github-actions[bot] 22f12e80e4 Regenerate generated files and fold pending files [skip ci] 2026-11-01 08:13:20 +00:00
semmelrinde 1b02f38732 German: translate the 20 messages the catalog lacks (Move Sketches to Their Parts, the view cube's arrows, view angle locks and more) (#10488)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 07:45:55 +00:00
Frank 7239c95858 Sketch Project: round edges project as arcs, not short lines (#10312)
Projecting a whole body fitted its outline from mesh edges: a round wall seen
edge-on has triangles leaning a hair either way, so the outline zig-zagged
between both rims and broke into two-point pieces (lines), and seams where two
faces' triangles don't share vertices counted as open, outline edges. Faces
that face one way all over (planes, cylinders and cones about the sketch
normal) now decide for all their triangles, near vertices are welded, and a
seam between two faces facing the sketch is no outline. A STEP hook went from
159 lines to 10 lines and arcs.

A single rounded or chamfered rim of a cut curved face is a spline that only
follows its circle; read as Bezier pieces it came in as twenty splines. An
edge read as spline pieces that stays on a circle within 1e-5 of its radius,
in a plane parallel to the sketch, now comes in as that arc.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 07:13:00 +00:00
clark19990 f90b6116d5 Measure: datum planes, axes and points, at exact distances (#10157)
* Measure: datum planes, axes and points, at exact distances

The Measure tool took faces, edges, vertices and sketch geometry; a datum
plane or axis could not be picked into it, and a point set of an infinite
plane or axis would measure the wrong thing anyway.

Describe origin and construction planes, construction axes and construction
points as items, accept PLANES, AXES and CONSTRUCTION_POINTS (and the origin)
in the Measure dialog, and give the new shapes exact closest points:

- a point and a plane: the perpendicular distance;
- a point and a line: the perpendicular distance;
- two planes: parallel distance, or zero where they meet;
- a plane and a line: zero where they cross;
- two lines: the skew distance.

The panel and the quick readout show a plane's point and normal, and an
axis's point and direction.

Test: a point 10 above the XY plane, 5 from the Z axis, and a plane and an
axis that meet.

* Select: a straight sketch line is a curve in an input that takes curves

Adding axes to Measure's input made a straight sketch line match the "axis"
rule before the sketch-line rule, so a picked external sketch curve became a
SketchCurve instead of the ExternalSketchCurve Measure records. The axis rule
now applies only to inputs that take axes on their own (Move's direction and
the like), not to inputs that also take curves.

Test: measure_input_fits_external_sketch_geometry, and the Move axis picks.

* rustfmt: format the new selection filter and measure code

* Measure: drop an unused normalisation in the skew-line distance

---------

Co-authored-by: clarkliu <277482660@qq.com>
2026-11-01 07:04:03 +00:00
semmelrinde 38b9ff223b Frames: a renamed cut-off piece keeps its name when the design is rebuilt from its features (#10438) (#10466)
* Frames: test that a renamed cut-off piece keeps its name in a rebuild from the features alone (#10438)

solidcraft-cli eval rebuilds a saved design without its saved model; the
rename made after a cut then lands on the first line's member. The test
reproduces it on main; the fix follows.

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

* Frames: a renamed cut-off piece keeps its name when the design is rebuilt from its features (#10438)

Since #10132 a cut's pieces past the first belong to the cut and the piece
that keeps the body's name to the body's own feature. Ordering the pieces
by their faces then moved the names among them but left each body's
feature behind, so the cut-off piece said the frame member feature made
it. body.rename stored its new name there, in the first line's slot, and a
rebuild from the features alone (solidcraft-cli eval of the saved file)
named member A after it. A name now moves together with the feature it
belongs to.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 06:43:40 +00:00
github-actions[bot] d96d547f6b Regenerate generated files and fold pending files [skip ci] 2026-11-01 06:53:11 +00:00
github-actions[bot] 05a8911f83 Regenerate generated files and fold pending files [skip ci] 2026-11-01 06:43:56 +00:00
semmelrinde 1581d85cba Czech: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10446)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:50 +00:00
semmelrinde ad54bdf8eb Danish: translate the 69 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10447)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:32 +00:00
C.Vengels 7bafc99390 Send to Slicer: the Print face input takes one face for each body sent (#10468)
With several bodies to send (after Add all bodies, say), the Print face
input refused every face. It took a print face only when exactly one
body was sent; any other pick was cleared at once, with a note in the
status bar only. Each body's print face then had to be set with Ctrl+B,
or by sending that body alone.

The input now holds the print face of each body being sent. A face
picked in it becomes its body's print face, in place of the one it had.
A click on a body's print face takes it out and clears only that body's.
A face of a body that is not being sent is refused with a message
(`ui.print_face_body_not_sent`, in place of `ui.print_face_one_body`).
Each change is still one `body.print_face` command, so it undoes on its
own. Picks in this window are no longer widened to the other pieces of
the face's surface, so a click on a print face takes it out rather than
adding its neighbours.

The user guide (guide/files.md) says so. The new message is in
assets/locales/pending/print-face-each-body.json, in all 31 languages;
the old one is removed from the catalogs.

Test: dialogs_save_mesh::tests::the_print_face_input_takes_one_face_for_each_body_sent
(fails without the change: the second body's face is refused).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:15 +00:00
semmelrinde e537a63988 Croatian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10449)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:11 +00:00
semmelrinde 984a5de001 Galician: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10448)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:11 +00:00
semmelrinde a799318b0f Hungarian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10450)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:44:00 +00:00
github-actions[bot] f989b3309f Regenerate generated files and fold pending files [skip ci] 2026-11-01 06:18:58 +00:00
github-actions[bot] 8771b734e4 Regenerate generated files and fold pending files [skip ci] 2026-11-01 06:09:49 +00:00
semmelrinde ef081519d5 Italian: translate the 62 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10453)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:43:48 +00:00
semmelrinde 2a3d29766e Indonesian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10451)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:43:38 +00:00
semmelrinde 058f17cc37 Malay: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10454)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:43:32 +00:00
semmelrinde 6475418169 Norwegian Bokmål: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10455)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:43:20 +00:00
semmelrinde 1a76f80bae Dutch: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10456)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:36 +00:00
semmelrinde f7ceba9cca Romanian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10457)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:34 +00:00
semmelrinde 249ca9267a Russian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10458)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:32 +00:00
semmelrinde 119168cb3e Turkish: translate the 67 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10461)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:19 +00:00
semmelrinde 2eb81a79b5 Swedish: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10460)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:19 +00:00
semmelrinde b503340b06 Ukrainian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10462)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:18 +00:00
semmelrinde 63123e9237 Serbian: translate the 68 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10459)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:42:05 +00:00
semmelrinde c843d1fb28 Traditional Chinese: translate the 67 messages main gained after batch 10, the regrouped Preferences included (batch 11) (#10463)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:41:16 +00:00
semmelrinde 483e21ab1b Sketch: Break Link in the context menu of projected geometry (#10417) (#10421)
* Sketch: Break Link in the context menu of projected geometry (#10417)

Projected curves and points (model edges and vertices, another sketch's
curves) could only be unlinked through command search, although Trim and
other refusals say "use Break Link on it". The sketch context menu now
offers Break Link when the selection holds projected geometry; text keeps
Explode Text.

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

* Sketch: test Break Link in a projected edge's context menu (#10417)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 04:41:25 +00:00
kaltenbrunnermarkus 9f37003dfa Browser: Features folders scrolled out of view skip working out their mark (#10439)
* Browser: Features folders scrolled out of view skip their mark

A folded Features folder shows the worst error or warning mark of the
features it holds. Working that out scans the design's whole history, and
from 16 features builds a table of all of them: per placement, on every
frame, whether the row is on screen or not. In a long tree of placements of
a part with a long history this was most of the window's frame while
orbiting.

A row scrolled out of view paints nothing (`tree_row` only takes its space),
so its mark is now worked out only when the row is on screen, as the
placement rows already do for their own state. The rows on screen show the
same marks.

Refs #6083

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

* Scale tests: time each frame of an orbit around a large design

`scale_tests::orbit_frame_cost` (ignored, manual) turns the view with the
right button over a design (a STEP file inserted as the app opens one, a
design file, or the generated assembly) with the model scene built, as the
app builds it once a renderer is there. Per frame it times the window
thread's frame and its named steps (`frame_watch`), egui's tessellation,
and the renderer's prepare, draw recording and GPU work on an offscreen
device (`OffscreenFrames::frame_parts`); without an adapter it times the
window thread alone. `painted` fingerprints the tessellated picture once
the view rests, to compare two builds.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 04:40:39 +00:00
Christian Glombek 4c44126a4d Sketch: sketch.create says which way the origin planes' sketch axes run (#10397)
On XZ a sketch's y runs along world -Z, so a value written as a height lands
below the plane; only the modelling skill's reference said so. The command's
parameter text now gives the axes of all three origin planes (XY: +X, +Y;
XZ: +X, -Z; YZ: +Y, +Z), and a test pins them.

Assisted-by: LLM
2026-11-01 04:19:07 +00:00
Christian Glombek 5c1aad8a09 Assembly: Relink Source takes a .json design, as Insert Design does (#10394)
`component.insert_design` links a design saved as a .sc container or as plain
JSON (`.json`), but `component.relink` refused every path that was not a
container ("choose a saved .sc design"), so a component linked to a JSON
design could not be pointed at another one. Relink now takes any design path
Insert Design takes (`is_design_path`: .sc, a legacy extension or .json); the
file is read by the same `design_bodies`, which already reads both. The Relink
Source file dialog opens on a filter that lists .json among the designs, too.

Assisted-by: LLM
2026-11-01 04:18:49 +00:00
kaltenbrunnermarkus 9d01f6e005 Sketch: construction lines with a length (#10258) (#10427)
* claim: #10258

* Sketch: construction lines with a length (#10258)

The horizontal, vertical, angle, parallel and perpendicular construction
line commands take an optional `length`: the line becomes a construction
segment from `at`, running towards `near`, held by a length dimension
that follows its expression. Without `length` the line stays infinite,
as before.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 04:13:03 +00:00
ashxddaga 127373a950 fix(ui): fade or unfade browser row dynamically when visibility toggles (#9953) (#9967)
- In entry_row (crates/ui-egui/src/browser.rs), use live  rather than cached  for the  property
- Ensures eye toggle dynamically fades or unfades rows for sketches and bodies without requiring a cache rebuild
- Add regression test in browser_card_tests

Fixes #9953
2026-11-01 04:10:32 +00:00
kaltenbrunnermarkus 64fe82b6c1 Engine: open a design a feature at a time (#10198) (#10274)
OpenSteps splits doc.open of a design into steps for a caller that must
keep working meanwhile (the web app, which has one thread): start reads
the file, each step evaluates the next feature by moving a copy's history
marker one feature on (so every result before it is reused), and finish
puts the design in the session's place with doc.open's reply. The next
feature's imported STEP text is offered first, so a caller can read it
in parts. Session::opened and opened_at are split into small helpers so
both paths build the session the same way; doc.open is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:39:22 +00:00
kaltenbrunnermarkus fafa575c9f Find a saved model's sheets by their text, not a record built per state (#10418)
Saving a design with its model cache asked, for every state, for each sheet-metal body it holds,
and built that sheet's JSON record each time only to find it written already. A design of 906
states with 300 sheets spent most of its save there. The sheet's text written straight from it
is the key now; its record is built only the first time. The records and the file are the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:20:05 +00:00
kaltenbrunnermarkus 7e701aabc1 Save: write the model cache manifest without copying the model's records (#10419)
* Find a saved model's sheets by their text, not a record built per state

Saving a design with its model cache asked, for every state, for each sheet-metal body it holds,
and built that sheet's JSON record each time only to find it written already. A design of 906
states with 300 sheets spent most of its save there. The sheet's text written straight from it
is the key now; its record is built only the first time. The records and the file are the same.

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

* Write the model cache manifest without copying the model's records

The manifest was built as a JSON tree, which copied the whole model's records (13 to 33 MB of
records in large designs) before writing them, and a cache too big for the file wrote it again for
each try at fewer states. The manifest is now written from the records where they are, its fields
in the sorted order the tree wrote them, so the file is the same byte for byte.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:19:59 +00:00
acebear17 9475e0e1a4 Docs/design approach 10063 (#10409)
* claim: #10063

* docs: add a design approach and workflows chapter to the user guide (#10063)

Adds guide/design-approach.md after the window tour: the parametric idea, principles for designs that change well, the timeline and rollback marker, bodies vs components and occurrences, common workflows with links, a term map for users of other CAD software, and where to find help.

* docs: match the chapter to the app's labels and sketch colours (#10063)

Checked in the web app (0.29.0): the timeline strip is headed History, the toolbar shows New Sketch and Parameters for Create Sketch and Change Parameters, and fully constrained geometry turns white in the default dark theme rather than dark.

* docs: tighten the design approach chapter's wording (#10063)

The glossary covers most, not all, of the chapter's terms; Redo moves forward; the timeline is shown as the History strip; Report a bug drafts a report for the user to file and sends nothing itself.
2026-11-01 02:27:40 +00:00
kaltenbrunnermarkus a8be28563c Sketch: leave out the exact roundoff estimate away from the pixel bound (#10267)
A flat node's roundoff allowance took a hypot per control point. Twice
|x| + |y| bounds each hypot from above with room to spare for rounding,
and rounding keeps sums monotonic, so whenever the flatness test passes
with that larger allowance it passes with the exact one too. Only nodes
whose chord distance comes near the 0.5 px bound (or with a non-finite
term) now take the exact hypots, and they decide as before.

Same result for every node: the reference comparison test still matches
bit for bit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:16:37 +00:00
NOWN-DEV a69a7c6ffe Inspect: measure names_only lists every world body without measuring (#10402)
A caller that only needs the bodies' names (assembly occurrences included,
e.g. "Body2 (Part:2)") had to measure every body, or read document.inspect,
which lists definition bodies only. `inspect.measure {names_only: true}`
returns each world body's name and its definition's name, honouring the
`bodies` filter; an unknown name is refused before anything is measured.
The MCP `measure` tool passes it through, directly and as a batch step;
`summary` leaves such a list as it is, since there is nothing to narrow.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:12:23 +00:00
Nathan Dane 7366fe0ecc Blur behind panels: render path, zero cost when off (#8876) (#9809)
* claim: #8876

* Add optional viewport-only Kawase blur and native UI composition

* Fix vendored panel hook imports and deprecated float constant import

* Drain texture updates in the full GPU blur fixture

* Hold the blur evidence fixture camera fixed across radii

* Address blur compositor lint checks

* Share animated Render preview background with blur source

* Use resolved Design background for blur source

* Exercise blur rendering independently of software adapter policy in CI

* Use stock egui hosts instead of vendoring the full crate

* Preserve feature-window open handling with stock egui hosts

* Target the native Window in close-handling regression fixture
2026-11-01 01:37:44 +00:00
MarkusRe 60d79e7ea2 Browser: show F2 shortcut on Rename context menu rows and route key when open (#10388)
Show F2 beside Rename on context menus where F2 already renames the
target (bodies, features and sketches, construction planes, components and
occurrences, browser groups, and canvases). Saved views and joints remain
without a shortcut label because F2 does not rename them without a menu.

When a context menu is open, pressing F2 routes through press_shortcut to
rename the menu's target (such as a clicked browser group or canvas) and
close the menu, rather than renaming the viewport selection.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gemini 3.8 Flash <noreply@google.com>
2026-11-01 01:37:25 +00:00
kaltenbrunnermarkus 653bc00be4 Sketch: work out each sampled point's clip distances and screen position once (#10343)
Each node of the display curve sampler recomputed a control point's
seven clip distances for every clip plane it tested (up to 7 times for
the reject test, 3 more for the depth test, once more for the cut), and
projected each point to the screen twice. They are now worked out once
per node into arrays and read from there.

Most nodes are split rather than drawn. A node is flat only when every
control point lies within the pixel bound of its chord after adding a
roundoff allowance that is never negative, so a point already farther
than the bound decides "split" on its own; the allowance (four hypot
and divisions per point) is now left out for those nodes.

Same values in the same order: the reference comparison test from the
previous commit still matches bit for bit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:12:51 +00:00
kaltenbrunnermarkus 915dd9a83f Viewport: the comment above build_sketch_scene describes the sketch scene (#10367)
The doc comment of the ground shadow had slid onto build_sketch_scene when
that function was added above ground_shadow. Give it back to ground_shadow
and describe what build_sketch_scene builds.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:12:43 +00:00
semmelrinde b99d4ddde9 Sketch Coincident: a selected line midpoint is a point it takes (#10311) (#10315)
* Sketch Coincident: a selected line midpoint is a point it takes (#10311)

A selected midpoint goes to the command as {midpoint: line}, as
Horizontal/Vertical's alignment takes it, but Coincident wanted a point
id and refused it with "b must be a point or curve". The command now
puts a point at the line's middle, tied there by a Midpoint constraint
(or uses the one already tied there), and makes that point coincident
or puts it on the other curve, in one undo step. The toolbar button and
the context menu send a selected midpoint that way.

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

* Sketch Coincident: test a line midpoint in the command, the toolbar and the context menu (#10311)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:44:48 +00:00
kaltenbrunnermarkus 425417b36a Web app: open a large design without freezing the page, with progress and Cancel (#10198) (#10211)
* Engine: open a design a feature at a time (#10198)

OpenSteps splits doc.open of a design into steps for a caller that must
keep working meanwhile (the web app, which has one thread): start reads
the file, each step evaluates the next feature by moving a copy's history
marker one feature on (so every result before it is reused), and finish
puts the design in the session's place with doc.open's reply. The next
feature's imported STEP text is offered first, so a caller can read it
in parts. Session::opened and opened_at are split into small helpers so
both paths build the session the same way; doc.open is unchanged.

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

* Web app: open a design a few features a frame, with progress and Cancel (#10198)

On the web, a design to open is now a staged file load like a STEP file
(#1376): its features are evaluated for about 30 ms a frame through
OpenSteps, a feature's imported STEP file is read a solid per frame first
and held so the feature finds it read, the progress card counts the
design's features, Cancel works between steps, and the meshes are made
a few per frame afterwards. The desktop app keeps its worker thread.

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

* Web app: design links and the files window open designs with progress and Cancel (#10198)

Design links (?open=URL, as embedded viewers use), the files window and
designs opened from the computer went through the one-shot open_path,
which froze the page on a large design. They now use the staged open; a
design link becomes an unsaved copy under the link's name once the open's
reply arrives (SolveApp::open_path_answered).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:17:15 +00:00
kaltenbrunnermarkus b7a6967ef2 Web app: open a design a few features a frame, with progress and Cancel (#10275)
* Engine: open a design a feature at a time (#10198)

OpenSteps splits doc.open of a design into steps for a caller that must
keep working meanwhile (the web app, which has one thread): start reads
the file, each step evaluates the next feature by moving a copy's history
marker one feature on (so every result before it is reused), and finish
puts the design in the session's place with doc.open's reply. The next
feature's imported STEP text is offered first, so a caller can read it
in parts. Session::opened and opened_at are split into small helpers so
both paths build the session the same way; doc.open is unchanged.

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

* Web app: open a design a few features a frame, with progress and Cancel (#10198)

On the web, a design to open is now a staged file load like a STEP file
(#1376): its features are evaluated for about 30 ms a frame through
OpenSteps, a feature's imported STEP file is read a solid per frame first
and held so the feature finds it read, the progress card counts the
design's features, Cancel works between steps, and the meshes are made
a few per frame afterwards. The desktop app keeps its worker thread.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:16:01 +00:00
NOWN-DEV 68105938e2 Sketch: a text rebuilt with other words keeps the far-move walk (#10357)
A sketch holding a text whose words come from a text parameter is left
unsolved after the words change and its links then move far, with the
warning "the sketch constraints conflict (k1, ..., k10)". The same two
changes the other way round solve.

Repro: a sketch with three projected points, a construction rectangle on
them and a centred text along its far side, the text's words given by a
text parameter. Change the parameter's words, then move the source
geometry about 280 mm. Without the word change it solves; with it, the
sketch warns of conflicting constraints.

Other words are other outlines: the text is built anew with new points
and curves. The solve that walks a sketch from its stored state to a far
link move (and the check for sides the jump turned over) refused the two
sketches as not the same entities, and the single jump left alone failed.

`Sketch::stored_with_rebuilt_texts` gives the stored sketch as this one
has its entities when they differ only by rebuilt texts: the texts'
entities as they are now, every other point, radius, arc turn and
constraint value as stored. `Document::solve_sketch` walks from that, so
the points pair up again. Sketches that differ in anything else are
refused as before.

Test: sketch_project::side_tests::a_label_follows_its_source_far_after_a_text_parameter_gave_it_other_words
(fails without the fix with the conflict warning above).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:15:32 +00:00
ABDULLAH 11e283ba77 Arabic: reasoning effort label, view cube arrow note (roll term, tanween spelling), Look group heading (#10355) 2026-11-01 00:11:16 +00:00
kaltenbrunnermarkus c25c2f3a21 Sketch: sample display curves without a heap copy per subdivision (#10342)
The pixel-bounded curve sampler that draws sketch curves and profile
outlines ran again on every camera move, and each subdivision step
allocated: every span's control points were a Vec, splitting one in
half built a Vec per de Casteljau level, each node's projection was
collected into a Vec, and every drawn part grew from two vertices.

The control points now live inline in a fixed array (`Hull`, at most a
degree-9 Bezier), the halves are worked out in place with the same
`mix` level by level, the projection goes into a reused array, and a
new part starts with room for eight vertices. The arithmetic and its
order are unchanged, so the points are the same bit for bit.

Tests: `samples_the_same_points_bit_for_bit_as_the_reference` keeps the
previous sampler as a reference and compares paths, quality and budget
use over every segment kind, slanted and far-off planes, orthographic
and perspective views from far out to far in, clipped and unclipped,
with ample and spent budgets. Ignored timings:
`sampler_cost_against_the_reference` (render) and
`sketch_scene_cost_while_the_camera_moves` (ui-egui).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:10:37 +00:00
kaltenbrunnermarkus e0582668bb Web app: an opened design is drawn when its meshes cannot be made on a thread (#10362)
The web build cannot start the open-meshing thread. Its fallback counted
every body done but listed none ready, so whatever the file load opened
on the web drew no bodies and kept "Loading parts n/n" up for good. The
fallback now lists every body ready and the viewport meshes as it draws,
as the fallback meant to. Found while opening a design through the file
load in the browser (#10198).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:10:20 +00:00
Colin Faulkingham df04e7742f Assembly: let components extruded from reference faces move independently (#10289) (#10332)
* claim: #10289

* fix: preserve placement of face-derived components (#10289)
2026-10-31 23:51:18 +00:00
github-actions[bot] 56b77b405d Regenerate generated files and fold pending files [skip ci] 2026-11-01 00:09:18 +00:00
raykholo e4a67d1e30 Assembly: move sketches to the part they're drawn on (#10218) (#10219)
* claim: #10218

* Assembly: move sketches to the part they're drawn on (#10218)

`component.move_sketches.to_parts` moves each given sketch, or every sketch in the design, whose
plane is an exactly attached face of a part's body into that part through
`component.move_sketches`, as one undo step. Sketches with no single part to go to stay where they
are, and the reply lists what moved and what stayed, with the reason. The Sketches folder's menu
offers it for the folder's sketches (or the selected ones), and a sketch's menu for that sketch or
the selected sketches; the status bar says how many moved and why the others stayed.

`component.move_sketches` now keeps a face-attached sketch where it is on its face: the sketch is
authored in the new component's placement (the face's own when the component owns the face), and
its picked point and repair plane are carried into it. Before, a face-attached sketch moved out of
the design was left without an author placement and failed to evaluate.

* Tests: the moved part is free to move; face anchor path

* Tests: move the to_parts test to the component command tests

* Shorter label: Move Sketches to Their Parts
2026-10-31 23:42:47 +00:00
Dahak Builds ed9599d1e4 View angles card: per-axis zero buttons and locks (#4581) (#10344)
* claim: #4581

* View angles card: per-axis zero buttons and locks (#4581)

The readout card under the orientation gizmo already showed and edited the
view's yaw, pitch and roll in degrees. Each axis row now also has a zero
button that turns that angle to 0 degrees at once, and a lock: a locked
angle stays put while orbiting, turning with the arrow keys or using a 3D
mouse, and its field and zero button go inert, so a symmetrical view can
hold its yaw and roll at 0 while only the pitch is adjusted.

* clippy: drop needless borrow in keep_locked call

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 23:39:24 +00:00
semmelrinde 80fb02f0b8 Sketch Ellipse: Major radius and Minor radius boxes while drawing (#10279) (#10282)
* Sketch Ellipse: Major radius and Minor radius boxes while drawing (#10279)

The Ellipse tool showed no value boxes. After the centre it now shows
Major radius, after the end of the major axis Minor radius; a typed value
places the point at that size and becomes the ellipse's dimension, as for
a circle's Diameter. sketch.draw.dimensioned takes major_radius and
minor_radius for sketch.ellipse, so Dimension new shapes covers it too.

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

* Sketch Ellipse: test the typed radii in the tool and in sketch.draw.dimensioned, and say so in the guide (#10279)

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

* Sketch Ellipse: rustfmt the new engine test (#10279)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:18:35 +00:00
kaltenbrunnermarkus a5c8fa7be7 Viewport: faster highlight with many bodies selected (#10204)
* claim: faster highlight with many bodies selected

* Viewport: find selected bodies by name once per highlight build

With every body of a large assembly selected, each hover change built the
highlight again and looked every selected body up along the world's bodies
(`ModelState::body` scans them), which is quadratic: about 2.2 million name
comparisons on 2,116 bodies, some 3 ms of every hover change.

The selected bodies, faces and edges are now found in a map by name, made at
the first lookup. A name held by more than one body finds the first, as the
scan did, so the same bodies are lit in the same order.

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

* Viewport: measure hover changes with many bodies selected

An ignored scale test: every body of a design selected (a file named by
SOLIDCRAFT_SCALE_DESIGN, else a generated grid of 1,440 boxes), the pointer
moving from face to face, timing each hover change's highlight build headless
and as the app builds it on a GPU (bodies drawn from their plans, and from the
model scene's blocks). Run with `--ignored --nocapture`.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:14:05 +00:00
bherbruck beb35029a4 Release 0.29.0 (#10347)
Fold the pending design format changes (align-axis-mask, equation-variable-unit, link-file-digests) into format 46.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:20:15 -04:00
MarkusRe 4c3d1484c5 Parameters: show one rename tooltip per name (#10309)
Keep full names in the tooltip only when the name cell clips them, and keep
the double-click rename hint without stacking a second popup. Cover short
and long names, favorites, and double-click input with a regression test.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 22:48:33 +00:00
Colin Faulkingham e0c1b8adbd Sketch: snap to infinite construction-line crossings (#10011) (#10285)
* claim: #10011

* Fix snapping at infinite construction-line crossings (#10011)
2026-10-31 22:23:57 +00:00
bherbruck 604515cfb3 Test: hovering a large body checks the pick index doesn't block, without timing (#10325)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 22:23:51 +00:00
github-actions[bot] 57d789c0aa Regenerate generated files and fold pending files [skip ci] 2026-10-31 22:34:32 +00:00
github-actions[bot] 4e9656ea9a Regenerate generated files and fold pending files [skip ci] 2026-10-31 22:25:42 +00:00
Nathan Dane 07160d9366 Preferences redesign 3/11: page structure, aliases and profile card (#9875) (#9907)
* claim: #9875

* Preferences: split pages and preserve aliases and profiles

* Preferences: preserve moved-page regressions and complete translations

* Preferences: retain newly added settings in profiles

* Preferences: target Tools in tool-cursor persistence tests
2026-10-31 22:05:20 +00:00
Dahak Builds c111b5f2fb ViewCube: option to invert directional rotation arrows (#10254) (#10286)
* claim: #10254

* ViewCube: an option to invert the directional rotation arrows (#10254)

A new Navigation preference, "Invert the view cube rotation arrows"
(invert_navigator_arrows, off by default): with it on, an arrow around the
cube orbits the camera the way the arrow points, as in Fusion and Inventor,
instead of rotating the cube toward the side the arrow sits on. Left and
right, and up and down, trade places, and each tooltip names the action
actually performed. The curved roll arrows are not inverted: they name the
rotation they perform, not a direction.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 22:00:16 +00:00
Nathan Dane 4594d81f7a Gruvbox: light and dark themes with three contrast choices (#10292)
* claim: consolidate Gruvbox contrast variants

* Consolidate Gruvbox variants into light and dark contrast choices

* Check both original Gruvbox viewport gradient endpoints

* Reset Gruvbox scenario state before independent theme checks
2026-10-31 21:58:30 +00:00
raykholo 2e3448f348 Sketch: an option to make the part active when sketching on its face (#10215) (#10216)
* claim: #10215

* Sketch: an option to make the part active when sketching on its face (#10215)

Preferences › Sketch › "Sketching on a part's face makes that part active", off by default.
When it is on, a sketch started on a face of another component's body activates that component
at the picked placement first and is made there, as after an explicit activate. The part stays
active, so what is built from the sketch lands in the same part.

For scripts the same is `sketch.create` `activate_part: true`; the reply's `activated` names the
component and placement. On an error the active component goes back to what it was.

* Tests: a free part, no edge projection, the sketch on the face plane
2026-10-31 21:52:09 +00:00
github-actions[bot] e5c6e3ef1e Regenerate generated files and fold pending files [skip ci] 2026-10-31 21:57:31 +00:00
rugbymauri 57df47b763 Sketch: clicking the running tool's button ends it, so clicks select again (#10151) (#10263)
* claim: #10151

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Sketch: clicking the running tool's button ends it, so clicks select again (#10151)

The lit ribbon button of the running sketch tool started the tool over
when clicked again, so it gave no way out: every click in the view kept
drawing or measuring, and Ctrl+click never reached selection. Esc and
right-click already end a tool, but the pressed button looks like the
natural switch.

A second click on the running tool's button (large ribbon buttons and
the constraint grid) now ends the tool. Persistent analysis toggles keep
their own on/off. The old behaviour stays behind the Sketch preference
"Clicking the running tool's button ends it" (ribbon_click_ends_tool).

Fixes #10151

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 21:32:48 +00:00
PapaEcho 42145719a0 Measure: start with the current selection (#10169) (#10234)
* claim: #10169

* fix: initialize Measure from preselected items (#10169)
2026-10-31 21:26:40 +00:00
rugbymauri 64421a2cb9 3D mouse: turns a rolled or upside-down view the way the cap is pushed (#10083) (#10262)
* claim: #10083

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* 3D mouse: turns a rolled or upside-down view the way the cap is pushed (#10083)

In the default upright (turntable) orbit the cap's Spin rolls the view,
but its Tilt and Turn went straight into yaw and pitch, which ignore the
roll. Rolled upside down, every push moved the model the opposite way on
screen; rolled on its side, tilt moved it sideways.

The device now takes Tilt and Turn back through the view's roll before
the orbit, for the cap and for a device app over the navigation bridge.
An unrolled view behaves exactly as before; Free orbit was already
view-relative and is unchanged.

Fixes #10083

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 21:20:10 +00:00
ABDULLAH 711e41233f Arabic: orbit/pan/zoom verbs in the NX navigation help, System language choice, measuring bodies progress (#10272) 2026-10-31 21:19:33 +00:00
kaltenbrunnermarkus cf0508e5c8 Sketch: held solves that cannot succeed give up sooner (#6606) (#10208)
* claim: #6606

* Sketch: held solves that cannot succeed give up sooner (#6606)

A status-only solve (the held solves of a drag) only asks whether it
succeeds. When LM sits in a least-squares valley of conflicting
constraints it crawled on to its 500-iteration limit, and a held solve
runs LM up to three times. Such a solve now leaves a component once the
iteration has stalled far from a solution: over the last 20 iterations
the sum of squared residuals fell by at most 1e-5 of itself while the
largest residual is still above 1e-4 mm.

Successful solves are unaffected: on about 62,000 LM runs that succeeded
(the sketch and engine tests and 120 generated sketch scripts with 2,900
drags) the slowest still fell by 1.4e-3 over any 20 iterations while
that far off. Solves with an analysis keep iterating (their report names
the violated constraints), and live frames are unchanged.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:51:32 +00:00
Dahak Builds bace1a1228 Assistant: set the reasoning effort per API agent (#10241) (#10259)
* claim: #10241

* Assistant: send a per-API-agent reasoning effort on /chat/completions (#10241)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 20:48:42 +00:00
Colin Faulkingham bda8b6b2e8 Fix Touchpad navigation with focused tool fields (#10231) (#10253)
* claim: #10231

* fix(ui): allow touchpad navigation with focused tool fields
2026-10-31 20:48:28 +00:00
P'tite Tête 52765304cd fix: Sketch rectangles and centre circles take expressions (#10251)
- sketch.rectangle.center and two_point: a corner given with expressions is a fixed point that follows its parameters, as with sketch.line
- sketch.circle.center: a radius or diameter naming a parameter is a driving dimension; a string naming none is just the number; bare numbers read in the design unit
- A radius that is not a number is refused with its own message and no longer passed over for diameter
- sketch.circle.two_tangent: a string radius reads bare numbers in the design unit

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-31 20:07:36 +00:00
Colin Faulkingham ec526d2b3d Sketch: keep polygon inputs clear of the preview (#10035) (#10199)
* claim: #10035

* Sketch: keep polygon inputs clear of the preview (#10035)
2026-10-31 20:06:03 +00:00
ABDULLAH 3b3d93f333 Arabic: selection wording for the freehand lasso option, clearer view cube fit note, tanween spelling (#10249) 2026-10-31 20:05:41 +00:00
kaltenbrunnermarkus 05f30c1674 Browser: a click just past a feature's name selects it instead of opening its sketch (#10079) (#10213)
* claim: #10079

* Browser: a click just past a feature's name selects it instead of opening its sketch (#10079)

The Edit Profile Sketch button on a browser feature row began 2 points after
the name, so a click that landed just past the name, or a press on the name
that slipped a few points before its release, opened the profile sketch.
The button now starts 8 points after the name, wider than egui's 6-point
click slop; the gap selects the row like the name does.

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

---------

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:05:28 +00:00
Colin Faulkingham bba2fc1400 Report omitted print-face selections in command-script exports (#10230) (#10235)
* claim: #10230

* fix: report omitted print-face assignments in script exports
2026-10-31 19:58:48 +00:00
Dahak Builds 538d7fcb43 Language: follow the system language by default (#10138) (#10183)
* claim: #10138

* Follow the system language on a first start (#10138)

The language preference defaults to a new 'system' value: the OS locale
(sys-locale) is matched against the catalog list - exact tag, base
language, then English - and a System choice sits at the top of the
Preferences language list. A saved language still wins.

* Drop the explicit deref clippy flagged; regenerate THIRD-PARTY-LICENSES.txt for sys-locale

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 19:50:08 +00:00
Ben Rog-Wilhelm 8be56316f6 Sketch Chamfer: several corners at once, as Fillet does (#8998)
The three sketch chamfers take `points: [corners]` (a point reference or
{a, b} each), as sketch.fillet does: every corner is cut alike in one
edit, and a corner that can't be cut, or is named twice, refuses them all.
The Chamfer window sends every picked corner instead of refusing a second.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:28:14 +00:00
github-actions[bot] eda51873aa Regenerate generated files and fold pending files [skip ci] 2026-10-31 19:33:49 +00:00
Colin Faulkingham d4144ddf9b Select: resolve faces and edges from a point or index (#10171) (#10191)
* claim: #10171

* fix(select): resolve faces and edges from a point or index
2026-10-31 19:11:54 +00:00
Dahak Builds c39f44e190 Orbit ignores floating input fields passing under the cursor (#10128) (#10206)
* claim: #10128

* Orbit ignores floating input fields passing under the cursor (#10128)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 19:04:33 +00:00
clark19990 5b8d8416e9 Navigation: an NX mouse mapping (#10156)
* Navigation: an NX mouse mapping

NX's CAD1 scheme drags the middle button to rotate, middle + left to zoom and
middle + right to pan. The mouse mappings here hold one button at a time, so
the preset takes the middle drag with Shift (pan) and Ctrl (zoom) for those two
drags, leaves the right button to the context menu, and fits on a middle
double-click as NX does.

The help text says which NX drags the modifiers stand for.

* Navigation NX note: translations for the complete catalogs

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

* Navigation NX note: use the message id nav.nx.help in the pending file

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

---------

Co-authored-by: clarkliu <277482660@qq.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:44:45 +00:00
github-actions[bot] 1f2356fa7b Regenerate generated files and fold pending files [skip ci] 2026-10-31 19:06:16 +00:00
github-actions[bot] e29e32f197 Regenerate generated files and fold pending files [skip ci] 2026-10-31 19:00:09 +00:00
kaltenbrunnermarkus 76e009ab10 Viewport: blend grid levels while zooming instead of popping (#9570) (#9608)
* claim: #9570

* Cross-fade the viewport grid between levels while zooming (#9570)

grid_step switches the spacing in a step (1, 2, 5 times a power of ten), so
the whole grid swapped at once as the view zoomed through a switch and
popped over and over while scrolling. Near a switch the drawn grid now
draws the outgoing and incoming levels together, weighted by a smoothstep
over about 19% of zoom either side, each level's lines on their own places
on the plane so a fading level never shifts. A line both levels share is
drawn once; its colour and width blend between minor and major.

grid_step itself is unchanged, so sketch snapping, dash lengths and the
tools keep their spacing; the dominant drawn level is always grid_step's.
The viewport and the headless picture share grid_lines, so they still draw
the same grid.

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

* Fade out the fine grid before a non-nested coarser level appears (#9570)

The levels go 1, 2, 5 times a power of ten. From 1 to 2 and from 5 to 10
every coarser line is also a finer one, but 5 is not a multiple of 2, so
cross-fading the 2 and 5 levels drew lines at 0, 2, 4, 5, 6, 8, 10 mm at
once, which looked restless.

At that switch the finer level's own lines now fade out over the first
half of the cross-fade and the coarser level's own lines fade in over
the second, so the two are never drawn together. The lines both levels
share (every 10 mm, the finer level's major lines) stay at full strength
throughout, so the grid never goes blank. The cross-fade spans the same
zoom range as before, and the nested switches cross-fade as they did.

grid_levels keeps its shares (adding up to 1, grid_step's level the
stronger one); grid_lines draws a line only one level has as strongly
as that level's own lines. The headless and viewport tests now check
half strength at the nested 5 to 10 switch and no 2 or 5 mm line right
at the 2 to 5 switch; a new test zooms through 2 to 5 in four decades.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:35:49 +00:00
froglemonade2 441d67f9ff View cube and axis gizmo: a standard view centres and fits the model (#10091) (#10092)
* claim: #10091

* View cube and axis gizmo: a standard view centres and fits the model (#10091)

A new Navigation preference, "Fit the model after a navigator view change"
(`navigator_fit`, off by default): with it on, a face, corner or arrow of
the view cube and an axis tip of the gizmo look at the middle of the model
from the one distance that fits its bounding sphere, so Home, an opened
design and every view chosen on a navigator share one zoom and one centre
and only the direction changes. Off, both navigators turn in place as
before.

"Home and Fit size" (`home_fit_size`, 10–100 %, default 100 %) is how much
of the view between the panels and the controls the model fills when Home,
Fit, an opened design or a navigator view change frames it; 100 % is the
framing as before.

`view.navigate` takes an optional `fit: true` with a direction or a turn,
so programmatic callers get the same framing.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-31 18:34:13 +00:00
semmelrinde 6541a4e980 Assistant: the agent list and the boxes under the switcher keep the panel's margin (#10172) (#10179)
* Assistant: the agent list and the boxes under the switcher keep the panel's margin (#10172)

The agent switcher above them sits in the panel's side margin, but what
the panel draws under it before a connection (the Agent row with Add
agent and Refresh, the agents found, what isn't installed, the New chat
confirmation and the sign-in box) started at the panel's very edge, and
Refresh was cut off at the right. They now keep the same side margin.

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

* Assistant: test that the agent list keeps the panel's side margin (#10172)

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

* Assistant: the margin test passes the labels without a needless borrow (#10172)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:31:55 +00:00
thocsmith 85197c1224 Selection: a freehand lasso that takes what is fully inside it (#4376) (#10185)
* claim: #4376

* Selection: a freehand lasso that takes what is wholly inside it (#4376)

The SELECT panel gets a Lasso checkbox (UiState::lasso_select, ui.set
lassoSelect). With it on, a primary drag from empty space draws a
freehand loop; the release selects through the same path as the window
box: items whose points are all inside the loop and no segment crosses
it, so a concave loop leaves out a curve that bulges out of it. Shift
and Ctrl add as with the box. ui.drag takes via waypoints (and scenario
drag steps take more than two points) so agents and tests can draw it.

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

---------

Co-authored-by: thocsmith <193096300+thocsmith@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:31:21 +00:00
Colin Faulkingham ed66adac5c Keep renamed bodies visible when reusing hidden names (#10163) (#10175)
* claim: #10163

* fix(engine): preserve visibility when renaming into hidden names
2026-10-31 18:15:56 +00:00
ABDULLAH e88295ceba Arabic: isometric view wording in the Extrude view preference, equation curve range unit option and note, quoted menu names in the graphics card note, tanween spelling (#10189)
* Arabic: isometric view wording in the Extrude view preference

* Arabic: equation curve range unit option and note read naturally

* Arabic: tanween spelling in the new bold-font and delete-first messages, menu and option names quoted in the graphics card note
2026-10-31 18:15:34 +00:00
Kyle a.k.a. TechSquidTV aeca117045 Sketch: offset chains lines whose corners are coincident (#5019) (#8858)
* claim: #5019

* sketch offset: chain lines whose corners are coincident, not only shared

Lines drawn one by one meet at their own points held by Coincident
constraints. The offset walked shared points only, so each such line was
offset on its own, to the left of the way it was drawn: a closed
rectangle came apart, both horizontals moving up. The same split shows
next to a fillet or chamfer, whose other corners stay Coincident pairs.

Chain ends held by a Coincident constraint as one joint, the way
Sketch::chain() and axis_implied already do, so a closed outline offsets
as one closed shape.

Fixes #5019.

* test(sketch): cover coincident offset loops after trim and blends

* refactor(sketch): share and cache coincident endpoint groups
2026-10-31 17:37:38 +00:00
Dahak Builds 31326e9898 Frames: renaming a cut-off piece of a frame member renames a different member after a rebuild (#10123) (#10132)
* claim: #10123

* Frames: a renamed cut-off piece keeps its own name through a rebuild (#10123)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 17:32:32 +00:00
github-actions[bot] a9e9f8b82b Regenerate generated files and fold pending files [skip ci] 2026-10-31 17:36:03 +00:00
austin62jake 8558b37e44 Edit dialogs: a stored number shows with its unit, Fillet, Chamfer, Shell, Draft and more (#1936) (#8697)
* claim: #1936 (Fillet, Chamfer, Extrude, Shell and Draft edit dialogs)

* Fillet, Chamfer, Shell and Draft: a stored number edits with its unit (#1936)

Their edit dialogs showed a value stored as a bare number as it was, so a
fillet made with a radius of 2 opened as "2" and a chamfer angle of 30 as
"30", where the other dialogs, and since #2107 the Plastic ones, read
"2 mm" and "30 deg". The Fillet radius, the Chamfer distances and angle,
the Shell thickness and the Draft angle now go through shown_expr, as the
Plastic dialogs do.

Extrude is left as it is: its editor keeps an unchanged expression exactly
as stored (extrude_edit_tests), and writing the unit would change it.

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

* Offset Face, Stock, Offset Plane, Plane at Angle, Pipe and Emboss too (#1936)

The rest of the dialog_descriptors! table's length and angle fields that
copied the stored text as it was now go through shown_expr as well: the
Offset Face distance, the Stock margin, the Offset Plane offset, the Plane
at Angle angle, the Pipe diameter and wall, and the Emboss depth. Scale's
factor has no unit and still shows as stored.

The test also edits an offset face, a stock and an offset plane.

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

* Revolve, Thread, Rib and Web too (#1936)

The custom edit paths that copied a stored length or angle as it was now
go through shown_expr as well: the Revolve angle, the Thread length, and
the Rib and Web thickness and depth. The test also edits a revolve.

The Rectangular and Circular Patterns stay as they are, like Extrude:
their editors keep an unchanged definition exactly as stored
(preselect_tests::feature_mirror_and_patterns_reopen_as_feature_replays_even_for_body_creators).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 16:58:34 +00:00
Colin Faulkingham aa244bcee4 Fix copying drawn 3D sketch curves (#10134) (#10146)
* claim: #10134

* fix: copy drawn 3D sketch curves (#10134)
2026-10-31 16:53:47 +00:00
PaddleStroke eaf3acc297 Colour popover: step buttons, HSV fields and recent colours (#9234) (#9398)
* claim: #9234

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

* Colour popover: step buttons, H/S/V fields and recent colours (#9234)

Every colour field of the app opens the same ui_kit popover. It gains:
- step buttons beside each number (R, G, B and the new H, S, V): a press
  steps once, holding repeats, for live fine-tuning;
- H (degrees), S and V (percent) fields on the same gamma HSV as the square;
- a row of the last 12 colours picked in any colour field. A colour joins
  when an edit is finished; fine-tuning in one opening replaces its entry.
  The list is saved with the app's settings (`recent_colors`), hostile
  saved entries dropped and the list capped.

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

* Recent colours: keep the shared list outside ui_kit, crate-visible only (#9234)

The store and the per-frame sync with the saved settings move to
crate::recent_colors; the ui_kit colour popover only shows the list and
adds finished picks. No plain pub escape from the gallery's ui_kit rule.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:51:56 +00:00
semmelrinde c392791f2c Sketch drag: a step that throws points far from the sketch, or presses a line to no length, counts as failed (#8600)
* Sketch drag: a step that throws points far from the sketch counts as failed

A held step of a drag that moved a point more than 1000 times the sketch's
size plus the drag's way was taken as solved, so a drag could leave points
around 1.4e6 mm away while reporting it reached the cursor. Such a step now
fails like one the solver cannot solve, and the cursor correction refuses it.

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

* Sketch drag: a step that presses a line to no length counts as failed too

An end dragged past the point where its line would have to turn over (a
line held at its midpoint and by an angle, dragged past the midpoint; a
rectangle's corner dragged across the opposite side) pressed the line to no
length, where it has no direction left for its constraints. A step leaving a
line under 1/1000 of its length at the drag's start now fails like one that
throws points far away, so the drag stops just short of it (48 such drags in
4000 random sketches, none with this).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:45:35 +00:00
rvmzym5xv8-star 0fb2e9b06b MCP batch: refuse a command entry with an unknown key (#7854) (#7865)
* claim: #7854

* MCP batch: refuse a step with an unknown key (#7854)

A step's parameters under another key (`args`) were dropped and the
command ran without them. The batch is now refused before its first
step, naming the key.

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

* MCP batch: a step carrying `expect` is refused, saying it is the batch's argument

#9904 gave batch an `expect` argument and tested that a step carrying one runs with
nothing checked. That is the silent drop this branch refuses: the step is now refused
before it runs, and the error says `expect` belongs to the batch.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:26:23 +00:00
clark19990 b410c577e0 Light a feature's faces when it is selected, even after a later edit renamed them (#9848)
* Light a feature's faces after a later edit renamed them

Selecting a feature in the Browser or the timeline lights its geometry on the
model, by the face names the feature gave (`F<feature>:…`). A later edit that
moves or renames those faces (Offset Face, Replace Face, Delete Face) leaves
them named after whatever last touched them, so selecting the earlier feature
lit nothing.

When no face carries the feature's name, compare the model states either side
of it instead: a face whose name is new, or whose area, centroid or plane
moved, is a face the feature changed. Each is found on the body as it is now
by its name, or by a parallel plane and nearest centre, so a face a later
feature moved again is still lit.

The Browser row of the feature a picked face belongs to already lights; that
is unchanged (a body row lights too, as #3009 asks).

Test: a box raised twice with Offset Face; selecting the first raise lights
the face the second one renamed.

* Browser: light the feature of a picked face, not its whole body

Picking a face in the view lit the body's row as if the body were selected,
which in NX is not what a face pick shows: the Part Navigator marks the
feature the face belongs to. Faces, edges and vertices no longer light their
body row; the feature row of the picked face shows as selected instead
(`picked_feature`), while the session's selection stays the face or edge, so
Replace Face, sketches and the rest keep the pick they take.

The live face of the view still lights the face itself; the timeline already
marked the feature of a picked face and is unchanged.

Tests: the browser's 178 tests pass (selection arrays unchanged; only the
rows lit differ).

---------

Co-authored-by: clarkliu <277482660@qq.com>
2026-10-31 16:26:05 +00:00
semmelrinde d95b8208b3 Sketch Fillet: a radius of half a side rounds both its corners (#2500) (#8238)
The second fillet of a side whose radius is half its length needs all
that is left of the line, and was refused as too large. Such a fillet
now takes the whole line: the line goes, the arc ends where the line did
and is tangent to the curve that goes on from there. A 20 x 10 rectangle
filleted with r = 5 becomes a slot, a 10 x 10 square a circle. Several
corners in one call keep their arcs by id, as removing a line renumbers
the curves.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:15:20 +00:00
semmelrinde 99f8cff454 Sketch Fillet: a corner held by Coincident takes a fillet when a dimension measures to it (#7507) (#7778)
* claim: #7507

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

* Sketch Fillet: a corner held by Coincident takes a fillet when a dimension measures to it (#7507)

Before rounding a corner whose two lines meet only through a
Coincident constraint, Fillet and Chamfer weld the two points into
one, but not when anything else referred to the second line's point,
such as a dimension measuring to the corner: the fillet was then
refused ("the lines must share a corner point"). The point is now
welded as well, and what referred to it refers to the corner, the
same place. A point that a constraint names together with the corner,
or a linked point, is still not welded.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:15:04 +00:00
semmelrinde 3292cba4ad Sketch Fillet and Chamfer: two lines that share no end are brought to where they meet (#6912, #7759) (#8426)
Two selected lines that stop short of each other or cross were refused
with "the lines must share a corner point". Now each line's end on the
shorter side of their meeting point moves there first (extending a line
that stops short, cutting a line that runs past), and the corner is
rounded or cut as before. Parallel lines, ends held by other geometry
and lines whose length is held are refused and change nothing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:14:40 +00:00
semmelrinde 6fa023b3ab Sketch Break: refuse an offset's curves, as Trim and Extend do (#8645)
An offset's ends follow its source curves on every solve, so a break's new
ends were pulled back: a broken offset arc came out in three pieces going
the long way round, and the solve moved curves the break never touched.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:14:14 +00:00
lewis2e 510378d965 Sketch offset: an offset circle no longer reports a degree of freedom (#9264) (#9573)
The offset sets an offset circle's radius from its source after the solve,
but the solve still counted that radius as a free unknown, so a
dimensioned circle and its offset reported 1 DOF instead of 0. Lines and
arcs were not affected: the offset already holds their end points. The
solve now holds an offset circle's radius too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:12:29 +00:00
github-actions[bot] c8a9159ac5 Regenerate generated files and fold pending files [skip ci] 2026-10-31 16:47:49 +00:00
github-actions[bot] fca86a414e Regenerate generated files and fold pending files [skip ci] 2026-10-31 16:39:00 +00:00
Colin Faulkingham b76fcae341 Preferences: retain Show tool at cursor between sessions (#10033) (#10049)
* claim: #10033

* fix(ui): persist tool cursor preference (#10033)
2026-10-31 16:10:37 +00:00
phanoob e64202417f Pattern and Mirror dialogs: explain Object Type, and say why a sketch is not taken (#10057) (#10097)
* claim: #10057

* Pattern and Mirror dialogs: explain Object Type, and say why a sketch is not taken (#10057)

The Object Type row of the Rectangular, Circular and Path Pattern and Mirror
dialogs gets a tooltip saying what Bodies and Features are and that a sketch
is neither. A sketch clicked for their objects, in the view, the Browser or
the History, now shows a hint naming what to pick and what to do with a
sketch instead of being ignored.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:10:09 +00:00
PaddleStroke 6a4b5c90c8 Properties: measure faster, show progress, and stop when closed (#8706) (#8730)
* claim: #8706

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

* Properties: measure faster, show progress, and stop when closed (#8706)

Properties on a large assembly measured every placed copy of every body
afresh (transform plus three fine tessellations each), so it took minutes.
#5203 (in #8103) already moved the measurement beside the window; this
builds on it.

- A rigid placement reuses the body's own cached measurement: volume, area
  and counts unchanged, centre placed, box from the placed corners (quarter
  turns) or from the body's shared mesh placed (other turns). Scaled or
  mirrored placements are still measured as placed.
- Bodies are grouped by component once and the subtree traversal groups
  occurrences by parent once (both were quadratic).
- The measurement beside the window is watched by a Progress: the card
  says "Measuring... n of N bodies" with a bar, and closing it (or
  switching designs) cancels the worker at its next body instead of
  leaving it running.
- component.properties and occurrence.properties are answered on a copy for
  control-channel requests too, as inspect.measure is.

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

* Properties: count the measuring work instead of timing it in the reuse test (#8706)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:04:38 +00:00
Colin Faulkingham 2e981d66e8 Sketch: allow dimension edits with tangent caps (#4823) (#9494)
* claim: #4823

* fix: resize sketches with tangent offset caps (#4823)
2026-10-31 16:04:29 +00:00
PaddleStroke ef0914ff94 Design tabs: optionally keep each design's dialog, tool and toolbar tab (#9360) (#9656)
A switch between design tabs cancels the open dialog and tool of the design
left and shows MODEL. With the new Interface preference "Keep dialogs and
tools open when switching designs" (keep_work_on_tab_switch, off by default)
each tab keeps its dialog (with its picks and values), its editing state, its
tool and its toolbar tab, and has them back when it is shown again. A feature
still computing after OK is cancelled as before. A feature edit's saved source
revision follows the view refresh of the switch, so its Cancel still restores
the history.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:04:22 +00:00
froglemonade2 0476154973 Update Links: linked designs that changed while the tab was away come back as one undo step (#9743) (#9753)
* claim: #9743

* Update Links: linked designs that changed while the tab was away come back as one undo step (#9743)

Coming back to an assembly's tab read each changed link in an Update Links
run of its own, so each became its own undo step: one Undo took back one
component and left another copy of the same part at the new version.

component.update_links now takes `components`, a list, and goes on past a
design it cannot read: that link keeps its bodies, is named in `failed` and
gets a warning. The tab return sends one request with every changed link,
records as read only the ones that were, and says on the status line which
one could not be read.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:04:05 +00:00
Thodoris Tsagkogeorgas 5e265089c7 Sketch: offset corners trim when they cross and extend when they fall short (#10089)
The chain walk follows ends that share a point, a coincident constraint, or
the same position. The offset then meets at the intersection.
2026-10-31 16:03:31 +00:00
semmelrinde 31a0c99a2c Delete: the delete runs with what its check computed ahead, instead of building the history again (#9917) (#10090)
* Delete: the delete runs with what its check computed ahead (#9917)

With the history rolled back, a delete builds the history after the
marker to see what it breaks, once in the check that runs off the window
thread (Session::ahead) and once more in the delete itself, on the window
thread. The check's results were lost on the way: the dry run's scratch
session and the whole-history copy dropped what the model keeps, and the
check's kept results never reached the delete. The scratch and the copy
now keep what their model keeps, and the delete (at once or after the
confirmation) runs with the check's kept results, so it finds the
history there instead of building it again.

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

* Delete: test that a confirmed delete runs with what its check kept (#9917)

With the history rolled back, deleting a side of the plate's outline is
checked off the window thread and confirmed first; the confirmation
carries the check's kept results, and the delete adds nothing to them.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:19:57 +00:00
Dahak Builds ce3b0d4086 Hide the origin point behind bodies like the axes and planes (#10050) (#10103)
* claim: #10050

* Hide the origin point behind bodies like the axes and planes

The origin point is a flat dot painted after the 3D scene, so it always sat
over bodies while the axes and planes respect the through-bodies rule
(#2697, #2618). Ask the meshes the same question the depth test asks the
axes: is a nearer kept face in front of the origin along the view ray.

Fixes #10050

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 15:14:46 +00:00
austin62jake d782d45cce Sketch: a centre rectangle's centre lines up with the axes when dragged (#10086) (#10087)
* Sketch: a centre rectangle's centre lines up with the axes when dragged (#10086)

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

* Sketch: a centre rectangle's centre lines up with the axes when dragged (#10086)

A centre rectangle's centre is held at the middle of a construction
diagonal (Midpoint), on none of its curves. Dragged by the centre, the
rectangle's own lines stayed snap targets and held the centre at their
middle, so it never lined up with the origin; dragged as a whole, the
centre wasn't a moving point, so only the corners lined up.

drag_snap::moving_points now takes all dragged entities at once and
treats a point held at a line's middle as going where the line's ends
go: it moves with a line whose two ends move, and when it is dragged
its line and the shape round it are no targets.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 14:31:19 +00:00
Dahak Builds 9a44641970 Fix Circular Pattern dialog showing two fields active at once (#10066) (#10078)
* claim: #10066

* Keep one dialog field active at a time (#10066)

Pick fields drew their accent ring from the dialog's active-input index
while text editors drew theirs from egui keyboard focus, so a Circular
Pattern dialog could show Axis and Quantity active together. A pick
field now drops its ring while a text editor holds the keyboard, and
clicking a pick field (or its clear button) takes the keyboard back.
Selection routing (d.active) is unchanged.

* Format editor_has_focus onto one line

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 14:01:09 +00:00
austin62jake 2b1ca94e2d Navigation: wheel and trackpad gestures go on under a value box (#10061) (#10073)
* Navigation: wheel and trackpad gestures go on under a value box (#10061)

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

* Navigation: wheel and trackpad gestures go on under a value box (#10061)

Any area above the model took navigation's wheel input, so a two-finger
pan, orbit or zoom stopped as the pointer crossed a floating value box
(Extrude's distance), and none could start over one. Model-anchored
canvas areas (Surface::Canvas: value boxes, a dimension's editor,
readouts) scroll nothing: navigation's wheel, trackpad pan and orbit and
pinch now go on under them. Their clicks stay theirs, and with value
stepping on the wheel stays with the value under the pointer.

Container::area records the canvas areas it builds; navigation asks
whether the top layer under the pointer is one (container::is_canvas).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 14:01:05 +00:00
github-actions[bot] f30fce0fc1 Regenerate generated files and fold pending files [skip ci] 2026-10-31 14:25:05 +00:00
github-actions[bot] f4b74fcb1b Regenerate generated files and fold pending files [skip ci] 2026-10-31 14:17:58 +00:00
semmelrinde 59efc86826 Rename Body: a pasted placement's body renames by the name the browser shows (#10017) (#10026)
* Rename Body: a pasted placement's body renames by the name the browser shows (#10017)

The browser lists the bodies of a component's second placement by their
placed names (`KB (K:2)`) and the context menu's Rename sends that name,
but body.rename looked it up only among the model's own bodies and
refused it ("no body"). It now takes a placed copy's name as the body it
shows, as appearance.assign does (#9740): the body is renamed in its
component, so every placement shows the new name.

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

* Rename Body: test renaming a pasted placement's body by its placed name (#10017)

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

* Rename Body: the test also hides the copy first and undoes the rename (#10017)

A copy hidden by its placed name stays hidden under its new name, and
one undo brings both names back (checks from the review on #10026).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:48:14 +00:00
leValdemar19 24ce89e0fc Modify ▸ Delete with nothing selected asks for a pick instead of a parameter error (#9079) (#9114)
* claim: #9079

* Modify ▸ Delete with nothing selected asks for a pick instead of a parameter error (#9079)

Starting `timeline.delete` from the ribbon, the command palette or the
control channel ran it with no parameters, so the notice showed the
engine's check ("`features` must list features, or `groups` timeline
folders"). It now deletes what is selected, as the Delete key does, and
with nothing selected shows a hint saying what to pick, changing nothing.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:35:33 +00:00
rugbymauri a476e645b0 Browser: F2 renames a selected group (#10005) (#10013)
* claim: #10005

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* F2 renames a group clicked in the Browser (#10005)

A group has no selection of its own: clicking its Browser row only
records it as the picked folder, which V uses. F2's rename_selection
looked only at the selection, picked components and canvases, so on a
group it did nothing, or renamed a body that was still selected.

F2 now takes a clicked group row first, as V does, and opens the same
Rename box as the group's context-menu Rename, which commits through
browser.rename_group. This works for groups inside components as well.

Fixes #10005

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 13:35:26 +00:00
leValdemar19 3385d1c570 Viewport: dark parts keep their shape: linear-light shading and a sky fill (#7974) (#9028)
* claim: #7974

* Viewport: dark parts keep their shape: linear-light shading and a sky fill (#7974)

Preferences > Graphics "Keep the shape of dark parts" (off by default)
shades the viewport in linear light: the key light and eye fill over a
sky fill that grows as a face turns up, plus the 4% of that sky every
dielectric reflects (a little more toward grazing), encoded to sRGB
once. Near-black faces keep a gradient; light and mid-grey faces stay
within a few levels of the sRGB shading. The world's up direction and
the switch ride in a new `sky` vec4 at the end of the viewport uniform,
and the frame key, so turning it on redraws. Off, the shader takes the
old path unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:30:07 +00:00
austin62jake f0acea0992 Preferences: a shortcut recorded from a search result takes the key (#10043) (#10058)
* Preferences: a shortcut recorded from a search result takes the key (#10043)

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

* Preferences: a shortcut recorded from a search result takes the key (#10043)

A Preferences search lists matching shortcuts with their key buttons,
but a recording started there only counted while the Shortcuts page
itself was open, and the page skipped its recorder during a search: the
key went into the search box and ran its command behind Preferences.

keymap::capturing now counts a recording while a search shows, and the
recorder runs for it; clicking back into the search field stops the
recording, so the search keeps its own typing. The recording's message,
a taken key's Use anyway / Cancel choice and the next-step hint show
above the search's shortcut matches too (outcome, shared with the page).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 13:28:40 +00:00
Colin Faulkingham 6047c0d5a1 Viewport: keep the ground grid stable through grazing views (#10014) (#10031)
* claim: #10014

* fix(ui): stabilize the ground grid near the horizon
2026-10-31 13:28:34 +00:00
Dahak Builds 34c378c063 Save As: rename the design to the new file's name (#4907) (#10036)
* claim: #4907

* Save As: rename the design to the new file's name when its name was borrowed (#4907)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 13:22:10 +00:00
github-actions[bot] dc6e25250c Regenerate generated files and fold pending files [skip ci] 2026-10-31 13:48:06 +00:00
ashxddaga 2e22ff8035 fix(ui): disable bold in Edit Text when font has no bold face (#9940) (#9958)
* fix(ui): disable bold in Edit Text when font has no bold face (#9940)

- Check whether font has bold face via font_data_styled() before enabling bold toggle
- Automatically uncheck bold and disable bold button with explanatory tooltip for built-in font
- Prevent sending bold: true in sketch.edit_text when font lacks bold face
- Add unit regression test in sketch_text::tests

Fixes #9940

* style: format choice.rs in ui_kit

* i18n: move sketch_text.no_bold to pending locale catalog with complete translations

* test(sketch_text): assert disabled bold on built-in font without error (#9940)
2026-10-31 12:57:46 +00:00
rugbymauri cb9b50b11e Assistant: switching models no longer reports "Method not found" for session/set_model (#9970) (#10022)
* claim: #9970

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Agent chat: no error when an agent does not serve session/set_model (#9970)

An ACP agent can list models in the unstable `models` field of its
session/new reply without serving `session/set_model`; it answers
JSON-RPC -32601 and the Assistant showed that as an error each time the
model was switched. Agents that offer the model as a configuration option
already go through session/set_config_option. A -32601 reply to
session/set_model is now logged at debug level instead of shown; every
other error still shows.

Fixes #9970

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 12:51:26 +00:00
github-actions[bot] a22aeaaf93 Regenerate generated files and fold pending files [skip ci] 2026-10-31 13:19:38 +00:00
semmelrinde d4eeedbf95 Delete: in a rolled-back history, the check builds the history once when nothing breaks (#9917) (#9972)
* Delete: the check in a rolled-back history builds the history once when nothing breaks (#9917)

With the history rolled back, the check before a delete (which features
the delete would break) built the whole history twice, before and after
the change, with the marker removed. On a large design each build ran
every boolean after the marker: deleting twelve splines took half a
minute on the window thread. Only a feature that fails after the change
can be newly broken, so the history after it is built first, and when
nothing fails there the history before it isn't built at all.

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

* Delete: test that the check in a rolled-back history still reports what breaks (#9917)

Deleting a hole in the plate changes the plate and every cut after the
marker and breaks none of them: the check reports nothing. Deleting a
side of the outline breaks the plate, and the check still says so, with
a cache too small to keep a state.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:29:30 +00:00
Colin Faulkingham c512c8e86a Feature editor: close when its feature is removed (#9394) (#10027)
* claim: #9394

* Fix stale feature editors after Undo and deletion (#9394)
2026-10-31 12:26:46 +00:00
SilentWolf75 1bcc1125f0 Graphics: apply the chosen card, or say when it could not (#8341) (#9252)
* claim: #8341

Co-authored-by: Cursor <cursoragent@cursor.com>

* Graphics: keep a preferred card when surface marks it incompatible (#8341)

Prefer the saved adapter by exact then case-insensitive name, including when only listed as surface-incompatible, and explain in Preferences when another card is still in use.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Format and clippy

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:00:52 +00:00
ashxddaga 7c013333db feat(engine,ui): allow restricting point-to-point align to chosen axes (#9946) (#10015)
* feat(engine,ui): allow restricting point-to-point align to chosen axes (#9946)

* style: apply cargo fmt across crates

* fix(clippy): use filter then map instead of bool::then in filter_map
2026-10-31 12:00:17 +00:00
kaltenbrunnermarkus 7e0f8a9a4c History: build the row plan once per revision and draw only the visible rows (#9616)
* claim: History: build the row plan once per revision and draw only the visible rows

* History: build the row plan once per change and draw only the rows in view

The vertical History worked out its rows from the design on every frame: for each row it
searched every folder's last feature (cubic in features and folders), cloned every name, and
looked up each row's result, sketch and selection by a linear search. It also drew and
hit-tested every row, including the ones scrolled out of view.

- The row plan (features, folders' depth, extent, header places, contents and joints, the rows
  with listed sketches, which sketches have patterns) is built once per design change, keyed
  by session, revision, marker, counts and the "Sketches: under their feature" choice, and kept
  in the egui context; the visible order is kept beside it until a folder or fold opens.
- A feature row outside the scroll area's clip takes its place and publishes its handles, but
  is not drawn or made interactive; one being found, dragged or focused, or under an open
  menu, is drawn as before.
- Selection and results are collected once per frame; folder rows read their contents from
  the plan.
- The horizontal strip collects the picked features (selection and dialog items) once per
  frame and writes a chip's tooltip only while it shows.

Tests compare the plan with the per-frame searches it replaced on tangled folders in every
fold state, the kept plan with a fresh one after each kind of change, and the History drawn
with every row against the culled one (same places, same text in view, fewer rows drawn).
An ignored test times the panel on a large design.

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

* Drop a needless borrow

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-31 11:54:50 +00:00
SilentWolf75 18e200f262 UI: recover chrome after sleep wakes a black window (#8749) (#8952)
* claim: #8749

Co-authored-by: Cursor <cursoragent@cursor.com>

* Recover egui chrome after a long pause between frames.

Sleep/wake can leave the window black while the GPU device stays up: re-upload
the font atlas, clear cached icon textures, and repaint when the frame clock
jumps so the ribbon and panels are not stuck blank (#8749).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Format

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:54:16 +00:00
github-actions[bot] bd0f0f3471 Regenerate generated files and fold pending files [skip ci] 2026-10-31 12:39:18 +00:00
github-actions[bot] e39d741862 Regenerate generated files and fold pending files [skip ci] 2026-10-31 12:30:11 +00:00
austin62jake ea9ea7bba8 Sketch: a driven dimension nothing fixes any more takes a value again (#10018) (#10023)
* Sketch: a driven dimension nothing fixes any more takes a value again (#10018)

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

* Sketch: a driven dimension nothing fixes any more takes a value again (#10018)

A dimension placed while a constraint fixed what it measures goes in
driven, and stayed driven once that constraint was deleted: a
double-click only said it has no value to edit. The double-click now
tries making it driving on a scratch copy; when nothing else fixes it,
the value box opens at the measured value, and Enter makes it driving
(sketch.toggle_driven) at the typed value (parameters.change) in one
undo step. A refused value goes back to a checkpoint, so it stays
driven. While something still fixes it, it stays read-only as before.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 11:53:42 +00:00
sascha2101 07fa5e4f1f Sketch: Project gives its new curves the selected line type (#9945) (#10009)
* claim: #9945

* Sketch: Project gives its new curves the selected line type (#9945)

The sketch palette gives the drawing line type (construction, centerline, infinite) to the
curves a SKETCH › CREATE command reports under `curves`. Project reports what it made per link,
`links[].curves`, so its curves were never found and stayed normal lines whatever the palette
said. `apply_mode` now also reads the links' curves, so a projected line becomes construction,
centerline or infinite and a projected circle construction, in the projection's own undo step.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:40:14 +00:00
semmelrinde 2a51f42495 Serbian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#10001)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:25:34 +00:00
HiroKenEz 8f259d5b83 Sketch: the Line tool can switch to a tangent arc and back without changing tool (#9489) (#9553)
* claim: #9489

* Sketch: the Line tool can switch to a tangent arc and back without changing tool (#9489)

With Preferences > Sketch > "Rest on a line's last point to switch to a
tangent arc" on (off by default), leaving the chain's last point and resting
back on it for 0.35 s makes the next click a tangent arc, by the existing
sketch.arc.tangent command, and the same gesture goes back to lines. A label
at the cursor says which. sketch.arc.tangent's end can now name an existing
point, so an outline drawn this way closes.

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

* Sketch: the cursor label names the command in words (#9489)

The label asked the catalog for a message id that doesn't exist, so it showed
the id. It now takes the command's translated label, and the test checks the
words it paints. Moved clear of the pointer's own badge.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:21:59 +00:00
semmelrinde a6b9d5a5b2 Browser: a sketch shown before an Extrude still hides once it is used (#9954) (#9971)
Show/Hide put every sketch it showed in `shown_sketches`, the list of
finished sketches kept shown although a feature uses them. Hiding and
showing a sketch nothing used yet put it on that list, so once an
Extrude used it, it stayed shown instead of hiding itself as a sketch
never hidden does. Show now marks only a sketch a feature already uses;
one nothing uses is shown anyway and leaves the list alone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:19:34 +00:00
semmelrinde 18e4b9ea9f Bosnian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9999)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:19:27 +00:00
semmelrinde 7cccb2b52e Croatian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#10000)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:19:21 +00:00
semmelrinde 7d7a1ecfa5 Russian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#10002)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:19:04 +00:00
semmelrinde 1202a79e25 Danish: translate the 349 messages main gained after batch 9, the toolbar's command help included (batch 10) (#10003)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:18:58 +00:00
semmelrinde fbafdfdcb6 Ukrainian: translate the 349 messages main gained after batch 9, the toolbar's command help included (batch 10) (#10004)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:18:52 +00:00
semmelrinde 2744063631 Norwegian Bokmål: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9998)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:17:23 +00:00
Frank 8e53d5050e View corner Cancel: cancel an unfinished tool before the sketch (#6709) (#9584)
* claim: view corner Cancel cancels an unfinished tool first (#6709)

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

* View corner Cancel: cancel an unfinished tool before the sketch (#6709)

With OK and Cancel in the view's corner on, the cross took the whole sketch
even with a line half drawn. It now takes the steps one Esc takes first (a
drag, Select Other, the tool's points, then the tool) and cancels the sketch
only when nothing is unfinished. Esc and the cross share cancel_unfinished.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:12:36 +00:00
github-actions[bot] fde56d3c8b Regenerate generated files and fold pending files [skip ci] 2026-10-31 12:05:01 +00:00
github-actions[bot] 81a6a3dcde Regenerate generated files and fold pending files [skip ci] 2026-10-31 11:55:25 +00:00
Colin Faulkingham 3eb76a8b3b Agent chat: continue after creating or opening a design (#7526) (#9980)
* claim: #7526

* Agent chat: continue across agent-initiated design changes
2026-10-31 11:12:00 +00:00
semmelrinde 60de59d5a9 Czech: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9984)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:54 +00:00
semmelrinde ad0b5c9d05 Hungarian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9986)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:48 +00:00
semmelrinde ff08eb0043 Italian: translate the 371 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9987)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:41 +00:00
semmelrinde 6bfd53a55a Dutch: translate the 371 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9988)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:35 +00:00
semmelrinde 709cb62fe8 Romanian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9989)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:29 +00:00
semmelrinde ee4786cd8f Swedish: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9991)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:23 +00:00
semmelrinde 6a4fef151f Turkish: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9992)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:16 +00:00
semmelrinde d3574dd493 Traditional Chinese: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9993)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:10 +00:00
semmelrinde d2a3430147 Malay: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9995)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:04 +00:00
semmelrinde 598556bca9 Indonesian: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9996)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:10:58 +00:00
semmelrinde 80341ec13b Galician: translate the 373 messages main gained after batch 9, the toolbar's command help included (batch 10) (#9997)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:10:52 +00:00
github-actions[bot] 7be5d93e51 Regenerate generated files and fold pending files [skip ci] 2026-10-31 11:28:33 +00:00
HiroKenEz 5bb3b6717f Equation curves: the variable can carry its range's unit (#9333) (#9549)
* claim: #9333

* Equation curves: the variable can carry its range's unit (#9333)

A curve can set variable_unit "range" (or tick the new box in its window): the
variable is then a length, so a ^ 2 / (4 * t) is a length when a is one. Start
and end must both be lengths, or one a length and the other a plain number in
the design's unit; anything else is refused. Saved only when set, as one pending
format change; without it curves behave as before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:54:06 +00:00
kaltenbrunnermarkus 5b6e5815a3 Engine: lower the per-command cost of evaluating large designs (#9615)
* claim: Engine: lower the per-command cost of evaluating large designs

* Engine: lower the per-command cost of evaluating large designs

- The fingerprint texts of features are kept in one store shared by every
  worker, not per worker: a command evaluated ahead runs on a new worker each
  time, where the per-worker store was always empty and every feature was
  written out again. Same texts, same hashes.
- Pinning body names after a command keeps, per feature, the names of the
  bodies it made with the state they were read from, so it no longer searches
  every result's bodies (features x bodies per command), and finds results by
  their index before searching.
- Pinning fingerprints the timeline again only from the first renamed feature
  (or a pattern or mirror of features replaying one) when the model proves its
  fingerprints are the design's; the fingerprints are those of the full pass.
- The evaluation cache's byte count looks bodies and sketches up instead of
  searching them per body (bodies squared per feature). Same counts.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:51:16 +00:00
github-actions[bot] 376c313214 Regenerate generated files and fold pending files [skip ci] 2026-10-31 10:41:12 +00:00
Colin Faulkingham ab665ee3b3 Edit Sketch: open an existing sketch from the ribbon (#9705) (#9829)
* claim: #9705

* Fix Edit Sketch ribbon selection and chooser (#9705)
2026-10-31 10:13:35 +00:00
sascha2101 f0d47e1d26 Preferences: turn the view to isometric when Extrude opens, and back afterwards (#9176) (#9948)
* claim: #9176

* Preferences: turn the view to isometric when Extrude opens, and back afterwards (#9176)

Two new preferences on the Design page, next to "Extrude to a picked target", both off by
default:

- `extrude_view_isometric`: each time Extrude opens, the view turns to the isometric view, so
  the depth can be seen right after a sketch seen face-on.
- `extrude_view_return` (only with the first): when Extrude closes, by OK, Cancel or Esc, the
  view goes back to where it was before it turned, keeping the projection as the end of a sketch
  does (#7038). The check is per frame, so every way the dialog closes is covered.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:13:08 +00:00
Dahak Builds a2365228ab eval_more: one FaceSlice value for the face helpers (#2684) (#9960)
* claim: #2684

* eval_more: one FaceSlice value for the face helpers (#2684)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 09:52:29 +00:00
froglemonade2 e2a7a45c44 MCP: batch and execute take expect {volume, bodies, bbox} and flag a mismatch (refs #3380) (#9904)
* MCP: batch and execute take expect {volume, bodies, bbox} and flag a mismatch (refs #3380)

An optional `expect` argument on the `batch` and `execute` tools: the volume
(with a relative `tol`, default 0.002), body count and bounding box (0.05 mm)
the agent computed before building. Once the call has gone through the server
measures the design as the `measure` tool does and the reply gains
`expectation: {ok, measured, expected, mismatch}`. A mismatch flags the tool
result `isError` with the mismatch text first; the design is kept as built.
A malformed `expect` is refused before anything runs. Without `expect`
nothing changes, and a batch step carrying one is a step with a stray key.

In 29 logged agent sessions every agent computed these numbers beforehand
and then spent a `measure` call plus a comparison by hand after every stage;
one missed a 0.25 mm skin until it compared. Volume and body count are
kernel-independent checks.

Refs #3380

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

* MCP expect: filter then map for the bbox coordinates that are off (clippy)

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

* MCP: expect's coordinate text helper no longer shares a name with the JSON reader

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: bherbruck <tenbee91@gmail.com>
2026-10-31 09:48:08 +00:00
kaltenbrunnermarkus fa21db7df8 Save: hard-link the backup copy and check for unsaved changes once per revision (#9617)
* claim: Save: hard-link the backup copy and check for unsaved changes once per revision

* Save: keep the backup as a hard link and check for unsaved changes once per revision

The backup of a save is the old file under a second name (a hard link renamed over the
old .bak) instead of a copy written out again; where links are refused (FAT, shares,
cloud folders) or the old file cannot be flushed, it is copied as before. Every point a
save can stop at leaves the design file whole and the backup the version before it.

Session::is_dirty compared the whole document with the saved one each time it was asked,
which the window does every frame for every tab. The answer is kept per revision and pair
of documents; the cache holds both, so neither can change in place while it stands, and
doc_mut lets it go first so edits stay in place.

On the rover in two unsaved tabs an idle frame takes 15.0 ms instead of 19.3 ms; a save
with its backup of the 11.8 MB file writes in 9.6 ms instead of 18.9 ms.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:29:20 +00:00
github-actions[bot] 4fda59af80 Regenerate generated files and fold pending files [skip ci] 2026-10-31 09:45:43 +00:00
MarkusRe 776dfc9c18 Agent chat: the system note reaches agents beyond the Claude adapter (#9811) (#9818)
* claim: #9811

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

* Agent chat: the system note reaches agents beyond the Claude adapter

The note only went out as `_meta.systemPrompt.append`, an extension of the
Claude adapter, so Codex and Antigravity never saw it. It now also goes in
the standard ACP `systemPrompt` field for agents that offer it, and in
`_meta.antigravity.systemPrompt.append` for Antigravity.

Fixes #9811

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:16:41 +00:00
P'tite Tête 6583c28231 fix: French catalog complete, 342 missing messages translated (#9941)
- 247 help texts (help.*) and 95 UI messages: theme options, viewport status, quick measure, personal sheet-metal rules, analysis details, sketch report
- Terms aligned with fr.json; no-break space before : ; ? ! as in the catalog
- Pending file, folded on main after merge

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-31 09:16:34 +00:00
N3MI-DG 6b24775413 Extrude: with every body hidden, a new extrude stays New Body instead of turning to Cut (#9919) (#9920)
* claim: #9919

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

* Extrude: with every body hidden, a new extrude stays New Body instead of turning to Cut (#9919)

Smart Extrude passed the shown bodies as `participants` to keep hidden
ones out (#6901), but with none shown that list was empty, and the
engine reads an empty list as the whole scope: a profile on a hidden
body chose Cut, which has no shown body to change, so nothing was
previewed and OK did nothing. With no body shown it now chooses New
Body.

The operation it latches for a side is also decided again when a body
is shown or hidden while the window is open, as a new start already
does (#467): showing the body an extrude goes into turns it to Cut,
hiding it again back to New Body.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:16:30 +00:00
austin62jake c4f9269409 Report a bug: the window can be dragged by its title (#9916) (#9934)
* Claim #9916

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

* Report a bug: the window can be dragged by its title (#9916)

The window was pinned at the top middle of the screen (ui_kit::Window
anchor), over the model, in a sketch or not. It now starts there and can
be moved (`placed`), as the file-load card can.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 09:16:04 +00:00
froglemonade2 9b31d29443 MCP: server instructions that say how to plan, build and verify (refs #3380) (#9903)
The `initialize` instructions were one paragraph of command shapes. Replace
them with version-neutral guidance: read `skill_reference solidcraft-modeling`
first, plan the numbers, build in a few `batch` calls, the primitive routes
and the fillet-shell-inside-holes order, what follows a parameter, `measure`
after each stage with a coalesced parameter check, and `timeline.edit` /
`parameters.change` / `edit.checkpoint` to fix instead of rebuilding. The
Assistant's system note gets the same pointer; docs/mcp.md says so.

`instructions_name_only_real_tools` now checks the command ids as well as the
tools, and `instructions_recipe_builds_a_parametric_shelled_box` builds a part
the way the instructions describe and checks its volume, axes and parameters.

Refs #3380

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-31 09:09:53 +00:00
froglemonade2 e16647a63f MCP: measure summary and body_topology faces: cylinders for small verification replies (refs #3380) (#9853)
* MCP: measure summary and body_topology faces: cylinders for small verification replies (refs #3380)

`measure {"summary": true}` replies one short object per body (name,
volume_mm3, bbox, faces, closed) with body_count and total.volume_mm3,
as a tool and as a batch step. `body_topology {"faces": "cylinders"}`
replies only the cylindrical faces, the kernel's split halves merged
by radius and axis line into {radius, origin, axis, extent, names},
and no edges. Both defaults are unchanged. The merge uses only what
model.faces and model.edges already reply.

Measured on a 13-feature enclosure, body_topology is 74 KB and measure
1.1 KB per body; agents filter them with scripts, and seven of 29
benchmark runs asked for exactly a cylinders-only listing.

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

* MCP: keep body_topology's cylinder merge next to check_parameters' without a name clash

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: bherbruck <tenbee91@gmail.com>
2026-10-31 08:56:37 +00:00
Wenyao Ma 5281734a22 UI: add theme-aware button hover and press feedback (#4984) (#5331)
* fix(ui): add theme-aware button interaction feedback (#4984)

* fix(ui): preserve footer button identity in overlapping dialogs

* fix(ui): preserve native button widths in toast actions
2026-10-31 08:32:45 +00:00
austin62jake b5fe8776b5 Assembly: default names skip names in use (part of #9738) (#9898)
* Claim part of #9738

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

* Assembly: default names skip names in use (part of #9738)

Contact sets, motion studies and exploded views were named by how many
there were, and a component's placements by how many it had, so after a
delete the next default name repeated one still in use, and commands
given that name acted on the first. They now take the first number no
item has, as joints' and the app's dialogs' default names already do; a
placement keeps the next number when it is free.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 08:28:50 +00:00
Dahak Builds 8986a670e5 3-Point Circle keeps snapped clicks as coincident constraints (#9858) (#9922)
* claim: #9858

* 3-Point Circle keeps snapped clicks as coincident constraints (#9858)

The tool sent only the three clicked positions, so a click snapped to a
point or curve left no constraint and the circle was free once drawn.
Each click now goes as its snap, as the 3-Point Arc's do, and the engine
adds a PointOnCurve for each referenced point, as the centre circle's
through click does (#3099).

* Fix #9858 test: assert the clicked midpoint point, not the line end

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 08:27:13 +00:00
froglemonade2 c528964363 MCP: check_parameters tries parameter values and puts the design back (refs #3380) (#9910)
A new tool that checkpoints the design, sets every value in one
parameters.change, reports measure, errors and cylinder axes, and
restores the checkpoint, so the loop set_parameter / measure /
body_topology / set back costs one call and one rebuild.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-31 08:24:41 +00:00
github-actions[bot] 251c20bef0 Regenerate generated files and fold pending files [skip ci] 2026-10-31 07:53:06 +00:00
austin62jake 3c0d4e120e Linked designs: one changed while the assembly was closed is read again when it opens (#9739) (#9872)
* Claim #9739

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

* Linked designs: one changed while the assembly was closed is read when it opens (#9739)

A link now keeps the SHA-256 of its design file's bytes as the assembly
last read them (DesignLink::file_sha256, a pending format change), set by
Insert Design, Relink, Extract to File and Update Links. component.links
reports `changed` from it, hashing a file again only when its time or
size changed (vfs::design_path_stamp, behind the design-path rule). The
app treats a link it meets for the first time whose file changed as not
read yet, so opening an assembly, in its own tab or in place of the blank
design, reads those parts again as a tab coming back does.

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

* Linked designs: read what changed last when a design opens in place of the blank one (#9739)

A read is taken up only by the design revision it started from, and
adopt bumped the revision after starting it, so the read was dropped.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 07:28:24 +00:00
Badger B bfd6aa9b7d Add German translations, batch 9 (#7237) (#9915)
Adds the last 79 German translations for messages that exist in en.json
but are missing from de.json, in a new pending file as described in
assets/locales/pending/README.md. Covers the agent chat tool summaries,
the history, sending to a slicer, Perforate, the window system and the
web app.
2026-10-31 07:24:48 +00:00
ABDULLAH 44d7c1fb6f Arabic: viewport and camera terms in the new view help and tab colour options, parameter, K factor, trim strip, run-off and standoff wording (#9909) 2026-10-31 06:55:14 +00:00
github-actions[bot] db696ec24b Regenerate generated files and fold pending files [skip ci] 2026-10-31 07:05:30 +00:00
Badger B a69c5770a4 Add German translations, batch 8 (#7237) (#9902)
Adds 130 German translations for messages that exist in en.json but are
missing from de.json, in a new pending file as described in
assets/locales/pending/README.md. Covers the Render studio (lighting,
environment, sun, depth of field), bounded volume and interior scattering,
saved shots, the timeline messages, thread direction and the agent chat
and endpoint settings.
2026-10-31 06:23:53 +00:00
momysnow da23eb1377 Sketch dialog follows "Dialog buttons at top" (#9860) (#9884)
* claim: #9860

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

* Sketch panel follows Dialog buttons at top (#9860)

The Sketch panel drew Finish Sketch, Look at and Slice in a footer under
its options even with Preferences > Interface > Dialog buttons set to
Top. Panel::palette now reads the same published preference as the
feature cards and draws its footer row under the header, above the
options, when it is Top. With the preference off nothing changes.

Changes a default: no

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

* Docs: Dialog buttons at top also moves the Sketch panel's Finish row (#9860)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 06:23:41 +00:00
austin62jake 5d9e4d5f95 joint.drive: a suppressed joint stores nothing, and the reply says what was asked and why a value differs (#9742) (#9862)
* claim: #9742

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

* joint.drive: a suppressed joint stores nothing; the reply says what was asked and why a value differs (#9742)

Driving a suppressed joint answered moved: false with "H is suppressed"
but wrote the values into the joint, with an undo step, so the part
jumped when the joint was unsuppressed. A suppressed joint outside any
motion link now stores nothing.

The reply gains `requested` (the values asked for, before limits and
links) and `held`: each driven value that ended up elsewhere, with why
(a motion link sets it, as a screw's travel follows its turn; a limit;
contact; a closed loop). A drive that moves nothing gives the first held
value's why as `reason`, so driving a screw's distance alone no longer
answers moved: false without one.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 05:57:24 +00:00
Dahak Builds 75b3fb678c Sketch: conflict messages no longer name a nonexistent id or claim full constraint wrongly (#9735) (#9859)
* claim: #9735

* Sketch: conflict messages name real constraints, not the refused one's id or a false 'fully constrained' (#9735)

* fmt + clippy: contains over iter().any (#9735)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 05:56:42 +00:00
github-actions[bot] 827713cc2f Regenerate generated files and fold pending files [skip ci] 2026-10-31 06:14:31 +00:00
ABDULLAH 4cf104ea2c Arabic: clearer wording in the new Sketch ribbon help (undo, mirror and revolve names, turned part, polygon sizes, normal direction) (#9870)
Co-authored-by: bherbruck <tenbee91@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:53:32 +00:00
Badger B 4fd8f8475c Add German translations, batch 7 (#7237) (#9864)
* Add German translations, batch 7 (#7237)

Adds 130 German translations for messages that exist in en.json but are
missing from de.json, in a new pending file as described in
assets/locales/pending/README.md. Covers the parameter list setting and
the Render panel pages: GPU material overrides, nested dielectrics, saved
optics, output size, physical background, presentation filters, preview,
quality budget, regrade and the result area.

* German batch 7: render.result_sequence follows the catalog's Checkpoint term

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

---------

Co-authored-by: bherbruck <tenbee91@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:47:17 +00:00
github-actions[bot] 9fdf380286 Regenerate generated files and fold pending files [skip ci] 2026-10-31 05:35:14 +00:00
austin62jake affe37bcc3 Body names: inspect.interference refuses an unknown body; appearance.chain and appearance.list take a placed copy's name (#9740) (#9856)
* claim: #9740

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

* Body names: inspect.interference refuses an unknown body; appearance reads take a placed copy's name (#9740)

inspect.interference dropped a name that is no body, so a misspelled or
wrongly cased name left that body unchecked and two overlapping cubes
answered count 0. It now refuses the name, as inspect.clearance does,
naming the closest body when one is near enough to be a typo or another
case (cmd::no_body, on the existing edit_distance).

appearance.chain, appearance.list and appearance.replace's scope take a
placed copy's name (KB (K:2)) and a placement's (K:2) for the body and
component they show, as appearance.assign already does, through two
Session helpers: own_body_name (on body_frames) and component_or_placement,
which appearance.assign now uses too. A name that is neither is still
refused. select.set already refuses an unknown body on main (#9737).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 05:07:04 +00:00
PeppaWutz 9d8ab72da7 Ribbon tooltips: help for every remaining toolbar command (#9060) (#9868)
The last 106 commands on the backlog get the two-sentence help under
their tooltip, in English, German, Japanese, Chinese, Arabic, Hebrew and
Vietnamese: Surface, Sheet Metal, Plastic, Inspect, Construct,
Configure, Parameters, Library, View and the few single ones (Bill of
Materials, Frame Member, Pattern on Path, Convert Mesh, checkpoints).

With that the backlog is empty, so it goes: the test is now
every_toolbar_command_has_help, and the docs that mentioned the backlog
say so. The help tests use sketch.drag, which is not on the ribbon, as
the command without help.
2026-10-31 05:04:31 +00:00
Nathan Dane 160662ffec Tab colours: per-tab surfaces and fade (#8876) (#9808)
* claim: #8876

* Resolve theme tab surfaces with cached Oklab transitions

* Apply tab surfaces to unauthored Render preview backgrounds

* Allow quantized endpoint colours late in cubic Render fades

* Keep tab surface publication from restyling ordinary widgets
2026-10-31 04:39:12 +00:00
github-actions[bot] b5550d01c1 Regenerate generated files and fold pending files [skip ci] 2026-10-31 05:05:00 +00:00
PaddleStroke fd5d0e6738 Assemblies: mass, centre of mass and inertia per occurrence and rigid group (#9715) (#9717)
* claim: #9715

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

* Assemblies: mass, centre of mass and inertia per occurrence and rigid group (#9715)

assembly.mass_properties (and Session::mass_properties) gives, for every
occurrence and every rigidly-joined group, mass (kg), centre of mass (mm) and
the inertia tensor about it in world and frame axes (kg mm2), the input
the multibody dynamics work needs. Bodies are measured as Properties
measures them (kernel volume and centre, mesh gyration), once per body,
then moved by each placement and combined by the parallel-axis theorem.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:35:29 +00:00
PaddleStroke 0067dcab28 Settings: edit an added AI agent (#9619) (#9653)
* claim: #9619

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

* Settings: edit an added AI agent (#9619)

Agents you added in Preferences > AI get an Edit button between Test and
Remove. It reopens the Add agent dialog filled with the agent's name and
command, or its protocol, preset, endpoint, model and request timeout, and
Save changes the entry in place: same id, permission override and
always-allowed tools. The stored key is never read back into the dialog;
an empty key field keeps it (still only sent to HTTPS or this computer),
a new key replaces it. The control channel gets `edit_dialog`.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:33:18 +00:00
snowsh1ne 2f6ca7e46b Linux: optional system title bar (server-side decorations) (#9263)
On Linux the window always had no OS decorations and drew Windows-style
caption buttons in the application bar. SOLIDCRAFT_TITLEBAR=system, or
"titlebar": "system" in graphics.json, now opens the window with the
desktop's own title bar and buttons (server-side decorations where the
compositor offers them). The default is unchanged; Windows and macOS
are unaffected.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:12:39 +00:00
PaddleStroke c2f66107cf Sketch: paste copied sketch geometry into another design (#9258) (#9675)
* claim: #9258

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

* Sketch: paste sketch geometry copied in another design (#9258)

Sketch geometry copied or cut in a sketch (sketch.copy / sketch.cut) now pastes into a sketch of another open design with Ctrl+V or the context menu, at the cursor or at the copied coordinates, with the constraints and dimensions among it. A dimension whose expression names the copied design's parameters keeps its value, in its unit; a text keeps its numbers. SketchClip::for_another_design prepares the clipboard; the app keeps the last sketch copy beside the cross-design body/component copy.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:11:57 +00:00
github-actions[bot] ec5f4b8740 Regenerate generated files and fold pending files [skip ci] 2026-10-31 04:29:13 +00:00
Dahak Builds 91aefd5abc Refuse sketch values that collapse a curve in settle_value_edit (#9729) (#9837)
* claim: #9729

* Refuse value edits that collapse a sketch curve: settle_value_edit runs settle's shrunk/flung checks (#9729)

* rustfmt

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 04:08:20 +00:00
PaddleStroke f1c2fc8804 Assistant: a new tab keeps the agent's sign-in (#9293) (#9669)
* claim: #9293

* Assistant: a sign-in confirmed in one tab holds in every tab (#9293)

The latest sign-in evidence per agent is window state: keep it across
tab switches, new tabs and replaced designs instead of resetting it with
the tab's conversation, so a new tab no longer marks the agent as signed
out when discovery's login probe is older than the chat's proof.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:01:52 +00:00
Nathan Dane d7cba9e96d Preferences: theme options list and the opacity option (#8876) (#9807)
* claim: #8876

* claim: #8876

* Theme options: persist per-theme values and expose preferences contracts

* Restore selected theme mode after stopping a followed theme

* claim: #8876

* Preferences: render theme options and apply readable panel opacity

* Use floating-point scale expectation in theme preferences scenario

* Keep panel opacity colour construction inside theme tokens

* Complete theme options translations in Arabic Hebrew and Vietnamese

* Let standalone theme sliders fill their option cells
2026-10-31 04:01:29 +00:00
PeppaWutz 849bb392d2 Ribbon tooltips: help for the Sketch tools (#9060) (#9847)
* claim: #9060

* Ribbon tooltips: help for the Sketch tools (#9060)

84 sketch commands get the two-sentence help under their tooltip, in
English, German, Japanese, Chinese, Arabic, Hebrew and Vietnamese, and
leave the backlog: every Create tool (lines, rectangles, circles, arcs,
slots, polygons, splines, conic, ellipse, text, points, the 3D sketch
tools, Project, Intersect and the other reference tools), every Modify
tool (Trim, Extend, Break, Offset, Fillet, the chamfers, Move, Scale,
Transform, Blend Curve), the patterns and Mirror, every constraint,
AutoConstrain, Sketch Dimension, Create/Edit/Finish/Cancel Sketch and
copy, cut and paste. The help tests use inspect.zebra as the
undocumented example, since sketch.trim has help now.
2026-10-31 04:01:09 +00:00
thocsmith 3b77567932 Docs: joint flip needs x for point origins; checkpoints are single use, 16 kept (#9746) (#9844)
joint.create's flip text and the assemblies guide said two origins at one
point with the same z stay as they are; a point origin without x takes its
x from z, so a part turned about z turns back. Say so, and point to giving
x on both origins or joint.as_built.

edit.checkpoint and edit.restore_checkpoint now say that restoring or
forgetting uses the checkpoint (and later ones) up and that only the last
16 are kept, and the "no checkpoint N" error says why. The editing and
mechanisms skills no longer read as if a checkpoint can be restored twice.

Co-authored-by: thocsmith <193096300+thocsmith@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:48:13 +00:00
Homero Thompson 810a9558bd perf(#3892): index the hidden joints and folders of the view (#9823)
The browser asks about every joint of the design on every frame, and
the answer walked the list of hidden joints: a nested STEP assembly
hides thousands of them, so a frame cost grew with the size of the
design.

Session keeps the hidden joints and folders as sets and answers from
them. The sets are built when the view changes and kept until it
changes again: the revision says the design changed, and the lists
say the view changed under it (the view is replaced outside the
engine too, without a revision of its own).

The behaviour is unchanged: the hidden joint stays hidden, the shown
one stays shown, and undo and redo keep the hiding right.
2026-10-31 03:01:33 +00:00
PaddleStroke d7ae568790 Insert SVG: say why a drawing has nothing to import (#9641) (#9652)
* claim: #9641

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

* Insert SVG: say why a drawing has nothing to import, and read UTF-16 files (#9641)

An SVG with nothing SolidCraft draws (only an embedded picture, shapes
pulled from another file with <use>, or not an SVG at all) was refused with
"invalid parameters for sketch.insert_svg: the drawing has no geometry
SolidCraft reads". It now names the file and says why, pointing pictures at
Insert Canvas and Canvas to Sketch. SVG files with a byte-order mark or in
UTF-16 now read, and a gzip-compressed (.svgz) file is refused clearly.

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

* Insert SVG: decode UTF-16 with as_chunks (clippy)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:00:32 +00:00
PaddleStroke 43a549d967 Render: start the GPU path tracer setup only when the GPU image method is chosen (#9041) (#9668)
* claim: #9041

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

* Render: start the GPU path tracer setup only when the GPU image method is chosen (#9041)

Entering the Render workspace started the GPU path tracer setup even with
the default Raster image method. The setup compiles the path shader, which
some Vulkan drivers (Mesa RADV/ANV, NVIDIA) take gigabytes over: the memory
climb reported while orbiting in Render. The setup now starts only while
Render is open with the GPU path method.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:59:05 +00:00
Dahak Builds 7228e218bf Preferences: unapplied changes no longer reach the settings file (#9730) (#9831)
* claim: #9730

* Preferences: unapplied changes no longer reach the settings file while the dialog is open (#9730)

* Preferences: hold unapplied changes out of the settings file while the dialog is open

While prefs_window.snapshot is set, SolveApp::prefs() writes the snapshot's
values - the ones Cancel restores - so the periodic autosave, quit, window
close, web save and control channel can no longer persist a change that was
never applied. Apply re-baselines the snapshot; settings the dialog cannot
change keep saving live. Adds regression tests and updates the tests that
pinned the old behavior.

Fixes #9730

* prefs: bind the snapshot's shadow outside json! so the ui-containers guard reads it

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 02:58:02 +00:00
ABDULLAH 67b491671f Arabic: viewport term in the viewport status preferences and details, tolerance, flip and colour key wording in the details cards (#9836) 2026-10-31 02:57:10 +00:00
momysnow 60b9e9d188 Edit Joint: show the two sides and pick them again (#9686) (#9692)
* claim: #9686

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

* Edit Joint: joint.edit takes the two sides

joint.edit accepts a and b in the form joint.create takes them (read by the same origin_param), so a joint can move its attachment without being deleted. A side left out keeps its place. The two sides must stay on different occurrences, checked as joint.create does. Limits, values, expressions and the name are kept; a relation's values start again where its parts are.

joint.edit gets an input contract and reply type (schema/joints.rs), and leaves schema/unconverted.txt.

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

* Edit Joint shows both sides and picks them again

The Edit Joint dialog now shows the two sides in the same selection inputs as Joint, each pre-filled with the face or edge of its own component under the origin. OK sends joint.edit a or b only for a side picked again, so an untouched side keeps its origin exactly.

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

* Edit Joint: a moved side turns the part as joint.create would

joint.edit kept the joint's old angle when a side changed, so the part could turn by a half turn about its axis when the new origin's x lay the other way. When a side moves and no angle is given, the angle is worked out again as joint.create does (the least turn from where the part is now); a relation's angle is its tilt and stays.

The mechanisms reference says joint.edit takes a and b.

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

* Edit Joint: pre-fill sides by name, send the angle only when changed

A side shows the face or edge its snap names, the piece nearest its stored pick when the name has several, else the one nearest that pick; it no longer searches outward from the frame origin, which could show the plate beside a hole for the hole's wall. A point or frame side shows nothing. OK sends the angle only when the user changed it, so a side moved without it turns the part as joint.create would.

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

* Edit Joint: a moved side leaves an angle expression alone (#9686)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-31 02:27:54 +00:00
Nathan Dane ca005237a1 Theme files: options, overlays and requires (#8876) (#9805)
* claim: #8876

* Theme files: add options, overlays and requirements
2026-10-31 02:25:34 +00:00
Colin Faulkingham 213e05da5f Shortcuts: normalize and validate text bindings (#9733) (#9800)
* claim: #9733

* Fix text shortcut normalization and current bindings (#9733)
2026-10-31 02:25:15 +00:00
bherbruck 251d18588a Release: build macOS on macos-15 runners (macos-14 is retired on Nov 2) (#9804)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:11:55 +00:00
github-actions[bot] 97f48a9f63 Regenerate generated files and fold pending files [skip ci] 2026-10-31 02:55:13 +00:00
PaddleStroke 72a265e4fc Nightly action matrix: a sketch on the sensor enclosure lid's top keeps its attachment (#9465) (#9670)
* claim: #9465

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

* Face attachments: a piece of a face its certificate finds the other pieces of keeps that certificate (#9465)

The nightly action matrix has refused a sketch and an offset plane on the
sensor enclosure lid's underside (`examples/sensor_enclosure :: Lid/F2:top`)
since #8278. The lid's lip, joined below it, parts the underside into a ring
and a panel; the ring rests on the case and is named after the case's top,
so the lid's cap certificate (via certified_pieces) finds only the panel.
#8278 then refused the certificate because it doesn't find the picked face,
and the history-name fallback can't take a name made by another body's
feature.

A face that is no pattern or mirror copy and lies on the same oriented plane
as the pieces the certificate finds is another piece of that face: the
certificate stands for it, as before #8278. Copies (the #8278 case) still
fall back to their own history name.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:02:24 +00:00
PaddleStroke 3877ef08b9 Measure: drag the labels away from the part (#8510) (#9395)
* claim: #8510

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

* Measure: drag the labels away from the part (#8510)

The distance and X/Y/Z labels in the view follow a drag of the pointer and keep
a leader to what they measure; a double-click or a new measurement puts them
back. A press on a label picks nothing, starts no box selection and opens no
Select Other.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:00:23 +00:00
PaddleStroke cce337f83f Sketch: alignment guides from any hovered point while drawing (#9105) (#9673)
* claim: #9105

* Sketch: alignment guides from any hovered point while drawing (#9105)

While a drawing tool waits for a point, hovering a line or arc end, a
rectangle corner or a free point wakes the same temporary horizontal/
vertical tracking guide as a circle centre or line midpoint. Idle
hovering keeps centres and midpoints only. Preference
sketch_tracking_points (on) turns it off.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:00:07 +00:00
PaddleStroke 5304294107 Browser: a Joints folder under each component lists the joints that attach it (#9130) (#9404)
* claim: #9130

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

* Browser: a Joints folder under each placement lists the joints that attach it (#9130)

Each component placement in the Browser gets a Joints folder (closed at
first) listing the joints whose Component 1 or Component 2 is that
placement, each beside the placement it joins it to. The rows are the
root folder's joints: click picks, the eye hides the glyph, double-click
edits and the menu is the joint's menu. The root Joints folder is
unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:59:53 +00:00
PaddleStroke b8eb8d262c Settings: saved to disk as soon as they change, not only when the app closes normally (#8275) (#9711)
* claim: #8275

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

* Settings: written to disk in the frame they change, not only at a clean exit (#8275)

eframe saves app.ron every 30 s and when the window closes normally. A
change made since the last save was lost whenever the app didn't close
normally: ended from the Task Manager, stopped by a sign-out or shutdown,
or crashed or hung in the graphics driver on the way out. The next start
then showed the old settings (View navigator back to Axes, Display quality
back to Normal).

The desktop app now writes its settings and flushes them in the frame they
change (frames with input and the one after), as the web app already does
(#8644). Unchanged settings are not written again.

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

* Settings: write at most once a second, and only after input that can change them (#8275)

Review: pointer motion came in every frame while the mouse moved, so the
settings were serialized every frame; and some settings change in every frame
(a panel's width while its edge is dragged), which would have flushed app.ron
on the UI thread each frame. Only clicks, keys, text, paste, wheel and pinch
count as input now, and the file is written at most once per second: a change
made in between waits, a repaint is asked for when its second is over, and it
is written then, so an idle app still writes it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:57:54 +00:00
github-actions[bot] 28cbca72c5 Regenerate generated files and fold pending files [skip ci] 2026-10-31 02:26:00 +00:00
PaddleStroke 9d18034e13 Sketch drag: a line is never shrunk to a point to follow the cursor (#9062) (#9728)
* claim: #9062

* Sketch drag: a line is never shrunk to a point to follow the cursor (#9062)

A line's end on a horizontal construction line through the origin, dragged
up off it, made the construction line vanish. A horizontal row is
b.y - a.y, which a line a hair long meets at any tilt, so the drag's held
solves met the point-on-line by shrinking the construction line: to 1e-5 mm
on the first frame, 1e-7 mm by the tenth, and the end went up.

A drag step (held solve, the tangent-ends-held solve, and each cursor
correction pose) that shrinks a line or circle from over 1e-3 to under
1e-4 now counts as failed, as a step that wraps an arc does. The end then
stays on the construction line, which keeps its length.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:54:19 +00:00
PaddleStroke d61ec60480 Assistant: say when an installed agent can't start, and how to reinstall it (#9034) (#9655)
* claim: #9034

* Assistant: say when an installed agent can't start, and how to reinstall it (#9034)

An agent whose program is found but fails its `--version` check without
printing a version (npm's opencode.exe left as a placeholder when the
package's setup script did not run, which Windows refuses with "not
compatible with the version of Windows you're running") was listed like
a working one, and connecting only said "The agent stopped." with the
system's message.

Discovery now keeps why the program did not start: the spawn error
(Windows' `%1` replaced with the program) or, through a `.cmd` shim, the
first stderr line or the exit status. A probe that times out still says
nothing. The picker shows "Installed, but it does not start: ...", and
connecting first explains it with the agent's install command and Refresh.
The control channel status carries `problem`.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:52:59 +00:00
Dahak Builds c4538c53e1 Fix Move feature editor prefill showing wrong values (#9696) (#9803)
* claim: #9696

* Show an edited Free Move its stored turn as the X/Y/Z angles that remake it (#9696)

* cargo fmt

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 01:51:40 +00:00
ABDULLAH b6b0d80332 Arabic: tanween spelling in the new ribbon help, fastener and standard part placement notes (#9820) 2026-10-31 01:51:32 +00:00
github-actions[bot] 42166bd6b7 Regenerate generated files and fold pending files [skip ci] 2026-10-31 01:56:59 +00:00
Nathan Dane 5bd5be9543 Viewport status item: standard details card (#8613) (#8614)
* Keep viewport summary menus below modal owners

* Build viewport status details from shared blocks

* Keep details focus checks outside context data locks

* Preserve native editor Escape ownership in details cards

* Initialize strict contract test sessions directly

* Translate viewport status detail cards

* Use inferred slider options and test legacy Section editing
2026-10-31 01:27:12 +00:00
PaddleStroke 2f09c90542 Exploded View: grey it out until the design has two components (#9142) (#9659)
* claim: #9142

* Exploded View: grey it out until the design has two components (#9142)

On a part made of plain bodies (the sample plate) Exploded View opened its
dialog and failed on OK with "an exploded view needs two or more
occurrences". The command is now disabled until the design has two or more
occurrences, and its tooltip says why and which commands make components.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:18:34 +00:00
Colin Faulkingham 8311ae5c5d BOM: group purchased children under inseparable parts (#9744) (#9794)
* claim: #9744

* Fix grouped BOM children for inseparable parts
2026-10-31 01:17:58 +00:00
Dahak Builds 4973638b56 Selection follows body rename so Delete targets the right body (#9737) (#9789)
* claim: #9737

* Selection follows body renames; stale body selections are dropped (#9737)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 01:17:28 +00:00
rugbymauri 8a61069fbb Control channel: document.inspect reports undo, redo and path in the app (#9624) (#9632)
* claim: #9624

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Control channel: document.inspect reports undo, redo and path in the app (#9624)

The app answers document.inspect on a scratch copy of the session, which
has no undo or redo history and no file path, so the reply always said
undo 0, redo 0, path null. Take those three values from the live session
and put them into the reply.

Fixes #9624

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 01:15:03 +00:00
PaddleStroke 55bea620c7 Continuous orbit turns about the Orbit around point (#1044) (#9690)
* claim: #1044

* Continuous orbit turns about the Orbit around point (#1044)

With Orbit around set to the point under the cursor (or the origin), Continuous
orbit still turned about the view's centre. Cursor orbits move that centre off
the model, so the model swung across and out of the view while it spun. The
spin now turns about the origin, or the model point in the middle of the view
(else the middle of the bodies in view), held from start to stop.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:14:03 +00:00
rugbymauri c755e067f5 Sketch pane: stays still at 125 % display scaling (#9457) (#9598)
* claim: #9457

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Keep the sketch pane still at fractional display scaling (#9457)

Right-docked cards were placed by their right edge from last frame's
measured width, which flipped by one physical pixel each frame at
scales such as 125 % in the Docked layout. Place them by their left
edge from the width they are held to instead.

Fixes #9457

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-31 00:49:10 +00:00
Colin Faulkingham f73479ddf2 Preserve multiline fields and accept a BOM in parameter CSV imports (#9732) (#9772)
* claim: #9732

* fix(params): preserve quoted CSV records and BOM headers (#9732)
2026-10-31 00:46:57 +00:00
Dahak Builds 772815b4c0 Sketch Extend: only an end on a circle counts as held (#9731) (#9773)
* claim: #9731

* Sketch Extend: only an end on a circle counts as held (#9731)

grow_line's on-circle test was true for every point inside the circle; test distance to the circle instead. Also pin the new end point of a held-end extension onto the curve it stops on, as the free-end path does.

* Fix regression test harness usage and held-end setup (#9731)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-31 00:45:22 +00:00
Erlis Vidal 6d89de1f5a Construction planes, axes and points hide with their component's placement (#9790) (#9791)
construction_visible checked the item's own hide and its folder, but not
the placement its component is drawn at, so hiding Part:1 left Part's
planes, axes and points on screen. It now asks feature_placement_visible,
the check sketches already made; the placement path is computed in one
place for both.
2026-10-31 00:44:47 +00:00
ABDULLAH 6375529b44 Arabic: colour key wording in the analysis status help, browser eye note, print face highlight tip, constraint glyph note, parameters apply-live option (#9446)
* Arabic: colour key wording in the viewport analysis status help

* Arabic: tanween and direction-neutral wording in the browser eye position note

* Arabic: outline wording and no direction mark in the print face highlight tip

* Arabic: sketch term in the constraint glyph note

* Arabic: checkbox wording and discard verb in the parameters apply-live option
2026-10-31 00:43:02 +00:00
CaRLymx bdd7605d47 Spanish (es-ES): catch up with en.json (2026-10-31/01) (#9781)
Add 50 missing messages from upstream main (5f60d78):
- Sort/Order UI: 3 keys
- Parameters live apply: 4 keys
- Preferences viewport/parameters: 8 keys
- Render output profiles: 3 keys
- Sheet personal rules: 17 keys
- Sketch text formula: 1 key
- UI 3D mouse, agent chat, viewport status: 14 keys

Validations: i18n --check ✅, lists ✅, assets ✅
Placeholders: 0 mismatches
Keys: 4828/4828 (100%)

Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
2026-10-31 00:42:11 +00:00
momysnow ffbaf292f0 Browser: drag over the eyes to show or hide many rows at once (#9680) (#9681)
* claim: #9680

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

* Browser: drag over the eyes to show or hide many rows at once (#9680)

Press an eye and drag up or down over other eyes: each eye the pointer passes
takes the first eye's new state (shown or hidden), and the whole drag is one
undo step. A plain click is unchanged. Dragging near the list's top or bottom
edge scrolls it. A component's eye now goes through browser.visibility, so it
is undoable like a body's.

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

* Browser: fix the eye drag's undo merge and layering (#9680)

Merge the drag's steps from the undo depth noted at press, when the design
and visibility still match, instead of searching the history. Run the merge
every frame from logic(), so a release under an overlay is never missed. An
eye under a popup or tooltip no longer takes part in the drag. Adds a
regression test for a drag from the Origin's eye after a show.

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

* Browser: say when the eye drag's undo merge gives up (#9680)

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

* Gallery test: the eye-drag state key is not a renderable element

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: bherbruck <tenbee91@gmail.com>
2026-10-31 00:35:34 +00:00
github-actions[bot] 2b4f07a0e8 Regenerate generated files and fold pending files [skip ci] 2026-10-31 00:50:08 +00:00
PeppaWutz 4b1556d102 Ribbon tooltips: help for the Assemble and Insert tools and the Create primitives (#9060) (#9069)
* claim: #9060

* Ribbon tooltips: help for the Assemble and Insert tools and the Create primitives (#9060)

56 commands get the two-sentence help under their tooltip, in English,
German, Japanese, Chinese, Arabic, Hebrew and Vietnamese, and leave the
backlog: every Assemble tool (components, joints, contact, motion,
position), every Insert tool (files, images, 2D, library, links) and the
Model > Create drop-down primitives (box, cylinder, sphere, torus, cone,
coil, pipe, thread, rib, web, emboss, boundary fill, bounding solid).
Their drop-down rows now show a tooltip as well. The help tests use
sketch.trim as the undocumented example, since solid.rib has help now.
2026-10-31 00:22:31 +00:00
Colin Faulkingham 0fe5bf114c Timeline: resolve exact feature names before ID fallbacks (#9689) (#9748)
* claim: #9689

* Resolve timeline names before falling back to feature IDs
2026-10-31 00:08:24 +00:00
PaddleStroke 526cdfd247 Zebra: flat faces keep one stripe colour while the view turns (#2153) (#9761)
* claim: #2153

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

* Zebra: flat faces keep one stripe colour while the view turns (#2153)

A face whose normal lies in the upright plane through the view (every
horizontal face) reflects straight ahead, where the zebra's sideways
coordinate is zero: exactly on the edge between a black and a white
stripe. Rounding then picked one or the other from frame to frame, so
the face flickered while the camera orbited. Straight ahead is now white.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:03:59 +00:00
bherbruck 045cfed6a7 Release 0.28.0 (#9766)
Fold the pending design format changes (browser-nested-groups, point-cloud-properties, print-faces) into format 45.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:50:02 -04:00
github-actions[bot] 5f60d78658 Regenerate generated files and fold pending files [skip ci] 2026-10-30 23:45:03 +00:00
Dahak Builds 646eb97eaf motion.list gives study keys in motion.study units (#9741) (#9760)
* claim: #9741

* motion.list gives study keys in motion.study units

Angle keys came back in radians while motion.study takes degrees, so an
agent that listed a study and wrote it back changed every angle by a
factor of 57.3, silently. Convert key values on the way out like
joint.list does (degrees for angles, mm for distances), give each key a
unit and its joint's name, and add a round-trip regression test.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-30 23:03:40 +00:00
PaddleStroke 2cf8df2ef6 Section caps: a body touching another along a curved face keeps its own cap (#9407) (#9665)
* claim: #9407

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

* Section caps: a body touching another along a curved face keeps its own cap (#9407)

Seen through a cut, a far wall and another body's face lying on it (a shaft
in its bore, a ring in its housing) are the same surface faceted two ways, so
the marking pass let the other body's face win in streaks. The section's
passes now first count the faces behind the cut in the stencil (back +1,
front -1): where the count says the cut runs through a body but a front face
won, the back faces are drawn again pulled toward the eye by a few pixels and
take the cap back.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:01:28 +00:00
PaddleStroke 0096e694dd Parameters: an option to apply edits together, when you choose (#9279) (#9400)
* claim: #9279

* Parameters: an option to hold edits until Apply or OK (#9279)

"Apply each change as you leave its field" (on by default, remembered in
the preferences as parameters_apply_live). Off: an expression edit is
checked when its field is left but waits, without a preview; Apply or OK
sets every held edit in one parameters.change {changes}, so the design
rebuilds once and one undo takes them back. Cancel, the close button and
Escape drop them. Turning the option back on applies what waits.

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

* Docs: the parameters_apply_live preference in ui.prefs (#9279)

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

* Parameters: the hint says edits wait for Apply or OK when they do (#9279)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:57:45 +00:00
github-actions[bot] c29bd18190 Regenerate generated files and fold pending files [skip ci] 2026-10-30 23:16:06 +00:00
Badger B 8e9e270027 Add German translations, batch 6 (#7237) (#9759)
Adds 130 German translations for messages that exist in en.json but are
missing from de.json, in a new pending file as described in
assets/locales/pending/README.md. Covers the remaining CPU lighting and
reflection controls, render diagnostics, lens and film effects, GPU path
tracer options, HDR source details, the resource library and the raw
linear EXR options.
2026-10-30 22:51:12 +00:00
fkirill 1b927421f6 Restore Browser and source-body context during Extrude editing (#5573) (#5576)
* claim: #5573

* Keep Browser and source bodies in the Extrude edit context (#5573)

* fix: keep Browser feature rows after manual rollback

---------

Co-authored-by: Kirill Frolov <k.a.frolov@vk.team>
2026-10-30 22:03:03 +00:00
PaddleStroke 8d22b3a41a DXF export: the full R2000 file structure strict readers need (#9144) (#9662)
* claim: #9144

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

* DXF export: the full release 2000 file structure strict readers need (#9144)

The view and flat pattern DXF left out parts of the AC1015 file that a
strict reader expects: the CLASSES section, the VPORT, VIEW, UCS, APPID
and DIMSTYLE tables (with *Active, ACAD and Standard) and the ACAD_GROUP
dictionary under the root dictionary. They are written now, along with
$DWGCODEPAGE; the active viewport looks at the whole drawing.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:01:27 +00:00
PaddleStroke edcf02c0d4 Crash reports: a window opened after startup no longer labels the run 'during startup' (#8776) (#9661)
* claim: #8776

* Crash reports: a window opened after startup no longer labels the run "during startup" (#8776)

egui-wgpu reports "window surface" and "surface configure" for every native
window, also the ones opened after startup, such as the agent chat in its own
window. Each one overwrote the journal's "running" phase, so a later unclean
exit was reported as "during startup (surface configure, N s after launch)".
crash::startup_step records a startup step only until the app runs, and the
app's egui-wgpu hook calls it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:00:19 +00:00
rugbymauri b85d5a665c Browser: the recover-unsaved-work card always shows Recover and Discard (#9472) (#9636)
* claim: #9472

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* Browser: the recover-unsaved-work card always shows Recover and Discard

A window's scrolling body was only offered the height the card had the
frame before. A card first laid out while the page was still tiny (a
browser canvas not sized yet) kept a body of no height for good, so only
its title showed and the required choice could not be made. The body is
now offered the room the screen leaves and shrinks to its content, which
also covers the desktop recovery card and every other system window.

Fixes #9472

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-30 21:41:29 +00:00
github-actions[bot] a232df7751 Regenerate generated files and fold pending files [skip ci] 2026-10-30 22:20:32 +00:00
Badger B 4757c3cee6 Add German translations, batch 5 (#7237) (#9697)
Adds 130 German translations for messages that exist in en.json but are
missing from de.json, in a new pending file as described in
assets/locales/pending/README.md. Covers the quick measurement, a few
Preferences labels and a large part of the Render panel (adaptive
sampling, surface aging, camera background, CPU lighting and environment).
2026-10-30 21:27:08 +00:00
github-actions[bot] 736f334301 Regenerate generated files and fold pending files [skip ci] 2026-10-30 21:50:16 +00:00
semmelrinde e721004f62 Croatian: translate the 14 messages main gained after batch 8, new Preferences included (batch 9) (#9540)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:00:37 +00:00
rugbymauri 88d87729d4 Browser History: drag rows to reorder features (#9565) (#9599)
* claim: #9565

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* feat(browser): reorder features by dragging rows of the History in the browser

Let users drag features in the History shown in the browser to move them, with the same drop bar and checks as the History at the left, while the rollback marker still drags on its own.

refs: #9565
Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-30 21:00:29 +00:00
ABDULLAH c7597ea66f Arabic: render term in the CPU resource profile size notes, pan and tilt verbs in the 3D mouse sketch orbit note (#9693) 2026-10-30 21:00:22 +00:00
github-actions[bot] f3389c3af7 Regenerate generated files and fold pending files [skip ci] 2026-10-30 21:19:55 +00:00
Janiz-zzz a0a52f6657 Add Finnish (fi) UI localization (#9626) (#9631)
* Add Finnish (fi) UI localization (#9626)

A fi catalog ("Suomi") translated from en.json, all 4801 messages,
registered in LOCALES between es and fr, with
finnish_catalog_is_registered_and_translates_keys, the language listed
in docs/localization.md (registered and partial) with its terms, and a
pending attribution row.

Fixes #9626

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

* Finnish: the 3D mouse Restore defaults button gets its own label

It shared its text with Restore defaults elsewhere on the page, so the layout test found
the wrong button.

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

---------

Co-authored-by: Janiz-zzz <267742026+Janiz-zzz@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-30 20:46:03 +00:00
github-actions[bot] cc9787f80e Regenerate generated files and fold pending files [skip ci] 2026-10-30 20:50:14 +00:00
fkirill 8e80d98610 Pre-highlight whole bodies for body selection inputs (#5572) (#5575)
* claim: #5572

* Highlight whole bodies for body selection inputs (#5572)
2026-10-30 20:24:45 +00:00
semmelrinde f8a8114843 Malay: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9535)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:24:22 +00:00
rugbymauri 98bfe1af9c Viewport: a docked Assistant beside an inverted view area no longer crashes (#9492) (#9499)
* claim: #9492

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

* fix(viewport): keep running when the view area comes out inside out

Make the layout beside the docked Assistant give way instead of closing the app when a window reconfigure leaves the view area inverted.

refs: #9492
Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>

---------

Signed-off-by: Maurizio Monticelli <maurizio@whatwedo.ch>
2026-10-30 20:06:19 +00:00
Alex Lytvynenko ffbf9836cc Agent chat: support ACP configuration options and OpenCode V2 (#3305) (#3867)
* claim: #3305

* fix(agent-chat): support ACP configuration options (#3305)

Handle current select options for model, thinking effort and mode, with confirmed snapshots and legacy compatibility. Full quality gate attempted; draft delivery authorized with unrelated geometry/property/timing failures documented in PR #3867.

* fix(agent-chat): keep configuration selectors inline (#3305)

Use compact value pickers and bounded wrapping to preserve conversation space; verified at normal and narrow widths with the installed OpenCode V2 server.

* style(agent-chat): format tool events
2026-10-30 20:00:26 +00:00
Homero Thompson 806adb6c46 A sketch curve id that breaks a name is refused (#5858) (#9606)
* claim: #5858

* fix(#5858): a curve id that cannot be written into a name is refused
2026-10-30 19:53:51 +00:00
phanoob 250ed2f266 Parameters: the expression field opens wide while you edit it (#9358) (#9384)
* claim: #9358

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

* Parameters: the expression field opens wide while you edit it (#9358)

A parameter's expression that is too long for its cell opens over the
cells to its right while it has focus, wraps instead of scrolling, and
shows the live value or error under it. Enter still applies it and the
field returns to its column; a short expression stays in its cell. The
add row's expression field does the same.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:47:44 +00:00
bherbruck 56f07a5598 Hover test: allow the first hover half of the index time, not a third (#9638)
On a loaded runner the hover alone has taken 70 ms against a 190 ms index, which failed
unrelated pull requests. Waiting for the index would still cost all of it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:41:24 +00:00
rlartot dfdad35d08 Extrude: a sketch picked in the Browser or History takes all its profiles (#6106) (#6154)
* claim: #6106

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

* Extrude: a sketch picked in the Browser or History takes all its profiles (#6106)

With Extrude, Revolve or Emboss open, a plain click on a sketch row in the
Browser or on a sketch in the History now fills Profiles with every profile of
that sketch (the island inside another profile's hole is left out). Before, the
click only changed the selection, which the dialog never reads.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 19:29:34 +00:00
github-actions[bot] 1cef0d83df Regenerate generated files and fold pending files [skip ci] 2026-10-30 19:48:48 +00:00
PaddleStroke a99c57ffc7 3D mouse: optional orbit while a sketch is open (#2227) (#9406)
* claim: #2227

* 3D mouse: optional orbit while a sketch is open (#2227)

A new Navigation preference, "Turn sketches with the 3D mouse"
(space_mouse_sketch_orbit, off by default), lets the cap and a device
app turn the view while a sketch is open and seen square on, as the
mouse already can. Off keeps today's behaviour: pan and zoom until the
view is tilted away from the sketch (#6687). Restore defaults on the
3D mouse section resets it.

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

* 3D mouse: keep the settings list in order after the rebase

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-30 19:22:58 +00:00
Colin Faulkingham cdffdcbbef Selection mode: the Modelling SELECT panel shows and clears component picking (#9382) (#9485)
* claim: #9382

* Show component picking in the Modelling Select panel (#9382)
2026-10-30 19:03:10 +00:00
ABDULLAH 4f05716f69 Arabic: follow the shortened English of the quick measurement preference note (#9604) 2026-10-30 19:03:07 +00:00
semmelrinde f0567d6a48 Hungarian: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9528)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:57:35 +00:00
semmelrinde 6e9ffe83d2 Romanian: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9531)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:57:28 +00:00
semmelrinde 2647ec51fc Ukrainian: translate the 95 messages the catalog lacks (#9591)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:57:17 +00:00
AsTechEvolves 23587433e2 Assistant: say when a Claude reply hit the length limit or was declined, not that the model lacks tools (#9495) (#9500)
* claim: #9495

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

* Assistant: say when a reply hit its length limit or was declined, not that the model lacks tools (#9495)

The API client never read why an answer stopped, so a Claude reply cut
off at max_tokens while thinking (its thinking text is empty by default),
or a refusal, ended the turn as normal with nothing said. The chat then
blamed tool support: "The model didn't use SolidCraft's tools".

Reply now carries the stop reason: Anthropic stop_reason max_tokens and
refusal, or OpenAI finish_reason length and content_filter and a message
refusal. Any other value is a normal end. The turn ends with that reason,
which already keeps the tool-calling notice away, and the chat shows
what happened in place of the raw "(max_tokens)", for ACP agents too. A
tool call cut off at the length limit has incomplete input, so it is
neither run nor kept in the history.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:56:14 +00:00
Daniel Dimitrov 0414de7b98 Hole: each dropdown option shows the hole it makes while hovered (#9577)
* Hole: each dropdown option shows the hole it makes while hovered

Extents, Hole Type, Hole Tap Type and Drill Point list names only, so seeing
what an option makes meant picking it. Hovering an option now shows a small
section of that hole beside the list, drawn by the dialog's own section
painter from a copy of the form with the option applied.

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

* Hole: give the dropdown previews a gallery entry

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

---------

Co-authored-by: danieldimitrov86 <danieldimitrovdev@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-30 18:54:18 +00:00
Kevin Ravensberg 295bd0e165 Render: fit the product inside the picture's frame, after the saved setup loads (#5763) (#5764)
While Frame shows the picture's frame, the preview projects into that frame
rather than the viewport, but Fit Product and the fit on entering Render
framed the product for the viewport's free area, so a picture taller than
wide cut the product at its sides. The fit now uses the viewport layout
seen from the frame (the frame as the viewport, the free area inside it
with the usual 16-point margin).

Entering Render fitted before the saved setup replaced the draft, so a cold
workspace with a portrait setup fitted for the old draft's aspect; the fit
now runs once the setup in effect is loaded.

render.camera.fit takes the picture's aspect (0.000001 to 1000000) through
Camera::fit_aspect, which publishes only a valid fitted camera: an aspect
that would divide the distance into infinity is refused and the previous
camera, document and undo stay as they were.
2026-10-30 18:30:45 +00:00
github-actions[bot] 0b34d3590d Regenerate generated files and fold pending files [skip ci] 2026-10-30 19:12:20 +00:00
austin62jake aedbf0b1d4 Preferences: an option to keep value boxes out of the view (#9479) (#9547)
* Claim #9479

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

* Preferences: an option to keep value boxes out of the view (#9479)

Design > "Show values beside their handles in the view", on by default.
Off, a command's value box (Extrude's Distance, Fillet's Radius, a hole's
reference distances...) isn't drawn over the model; the command window's
value field takes the keyboard that opening the window or a pick gives the
box, its text selected, so a number typed after a pick still sets it. The
handles stay and still drag the value.

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

* Preferences: the value box option in Arabic, Hebrew and Vietnamese too (#9479)

The complete catalogs must carry every English message.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 18:30:04 +00:00
semmelrinde a13e0e0ceb Danish: translate the 1055 messages the catalog lacks outside the Render workspace (catch-up 1 of 2) (#9589)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:29:51 +00:00
semmelrinde 6f05c19c42 Danish: translate the Render workspace's 1245 messages (catch-up 2 of 2) (#9590)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:29:44 +00:00
HiroKenEz db0c25522e Render: a size the CPU profile can't render names the profile that can (#8941) (#8943)
* claim: #8941

* Render: a size the CPU methods refuse names the profile that renders it (#8941)

The Shots strip offers 4K UHD whatever the method. Under the default CPU
raster, and under CPU ray tracing's default Compact profile, Render was then
refused with English engine text that didn't say how to render that size.

The refusal is now translated and names the smallest CPU resource profile that
admits the size (with CPU ray tracing, for raster, which has no profiles). The
size lists mark such presets. Nothing is switched: method, profile and
resolution still change only when chosen, and GPU path tracing is unaffected.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:16:36 +00:00
CaRLymx e6c999bbcd Spanish (es-ES): catch up with en.json (2026-10-30) (#9569)
- Base: origin/main (71b3a70, post #9448)
- en.json: 4,778 keys → es.json: 4,778 keys (100% complete)
- 1,033 reusable translations from previous catch-up branch
- 88 newly translated keys (construction lines, point cloud, print face, quick measure, recovery, UI hints, update checks)
- Terminology per glossary: 'Servicio de API', 'Desactivar/Reactivar', 'alma/ala', 'Ángulo de plegado/Ángulo incluido', 'vinculación de superficie', 'toma/escenario/estudio/configuración', 'X-Líneas' (construction lines), 'cara de impresión', 'bisectriz', 'tangente común'
- 0 placeholder mismatches, catalog sorted alphabetically
- cargo xtask i18n --check ✅, cargo xtask lists ✅, cargo xtask assets ✅

Co-authored-by: carlymx <24634598+carlymx@users.noreply.github.com>
2026-10-30 18:07:52 +00:00
PaddleStroke 180a725c0e Parameters: sort the list by name within each section (#8563) (#8725)
* claim: #8563

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

* Parameters: sort each section by name (#8563)

The Name heading of every Parameters table steps through A to Z, Z to A and the
listed order. Each section sorts on its own: Favorites, User Parameters and each
model group (the groups keep the design's order). Names compare in natural order
(d2 before d10, letters ignoring case). The choice is kept in egui's persisted
memory like a section's open state. While sorted, a user parameter's move arrows
are disabled. The sortable heading and natural_cmp live in ui_kit::table.

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

* Parameters sort: gallery entry for the sortable heading, natural_cmp private

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:02:06 +00:00
Steven Waters 4ba6a9744b Browser: allow nesting groups and re-ordering groups & components (#9109) (#9116)
* Browser: nest groups and reorder components

Groups can hold other groups (up to 8 levels) and be reordered with the
items around them. Sub-components can be reordered too, through a new
`components` list in `browser_order` that is not drawn as a folder.

Document: `BrowserGroup` gains `parent`; group items and `browser_order`
lists hold `BrowserEntry` values (a number is a group, a string an item).
One pending format file; older designs read unchanged. Loading and
refreshing drop what the lists name that is gone, and cut cycles and
over-deep nesting to the top level.

Engine: `browser.group` takes `parent`; `browser.move`, `browser.order`
and `browser.ungroup` handle groups. A change to only the browser's
groups and order no longer re-evaluates the model.

UI: nested rows, dragging a group row (top of a row: before it, the rest:
into it), New Group inside a group, and dropping a placement on the top
of a sibling to reorder it.

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

* Fixed formatting

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

* Fixed unintended side effect in render scene browser test

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

* Preserve group order in render workspace

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

* Increased test coverage

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

* Updated clipboard test from upstream main for group item type

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 17:40:33 +00:00
Nathan Dane ae998906ff Viewport: fold three status items into a summary badge (#8549) (#8558) 2026-10-30 17:33:23 +00:00
phanoob 634f0a2308 Sketch text: number parameters in the words, and = for a formula in Edit Text (#9281) (#9386)
* claim: #9281

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

* Sketch text: a number parameter in the words shows its value (#9281)

A text's words expression can now name a number parameter: `'W' + Width`
reads "W85 mm" (lengths in the design's units, angles in degrees, up to
three decimals). `number(Width)` drops the unit and `number(Width, 2)`
fixes the decimals. The words and the cut follow a parameter edit in the
same rebuild, a rename reaches inside number(), and deleting the parameter
is refused while a text shows it.

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

* Sketch text: a Text starting with = builds its words from parameters (#9281)

In Edit Text, a Text that starts with = is the words' expression
(='W' + number(Width)): OK keeps it, the dialog shows it again with its =,
and an untouched OK no longer turns it into typed words. Typed words still
replace it, and a saved text that happens to start with = stays typed
until it is edited. The field's tooltip says how.

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

* Sketch text: the formula hint in every complete catalog (#9281)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:28:31 +00:00
phanoob 306b78ce16 Expressions: Down arrow in an empty field lists the parameters (#9246) (#9385)
* claim: #9246

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

* Expressions: Down arrow in an empty field lists every named parameter (#9246)

In an empty expression field, Down opens the completion list with every
named parameter (not just the first eight), armed on the first one; Up and
Down walk it, the list scrolls to keep the chosen row in view, and Enter
inserts it. A field that already holds text keeps Down for itself.

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

* Expressions: Preferences caps the Down-arrow parameter list (#9246)

Down arrow in an empty field lists the parameters up to "Parameters
listed by Down arrow" in Preferences > Design (default 50, 5 to 200),
favourites and the design's own parameters first, so a large design
does not build a list of hundreds of rows.

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

* Expressions: a label that fits for the parameter list cap (#9246)

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

* Expressions: the parameter list cap in every complete catalog (#9246)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:27:56 +00:00
Badger B 678da425da Add German translations, batch 4 (#7237) (#9554)
Claude-Session: https://claude.ai/code/session_01YVbV9BQxn5xzooBo8yEt9a

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:27:13 +00:00
froglemonade2 fc499441e2 Lost GPU: the old process exits instead of hanging in the driver, after the settings are saved (#9044) (#9054)
After a lost device the next start began from on_exit, while eframe was
still writing app.ron, and the old process then hung in
vkDestroySwapchainKHR on NVIDIA. Start it from Drop for App instead, after
eframe has joined the settings save and before it drops the window's GPU
surface, and end the process there after a loss. The update restart moves
there too. The loss reason is passed to the next start and shown in its
note.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:26:33 +00:00
github-actions[bot] 1cf5f83924 Regenerate generated files and fold pending files [skip ci] 2026-10-30 18:38:33 +00:00
bherbruck 6a465abee9 Quick measurement: stop the off-thread test failing when the thread is fast (#9566)
The test asserted the readout was still empty right after the first poll, but the measuring thread can finish first, so it failed now and then in merge groups and on unrelated pull requests. It now asserts only that the measurement arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:37:42 -04:00
github-actions[bot] 89a3e89d62 Regenerate generated files and fold pending files [skip ci] 2026-10-30 18:31:05 +00:00
Cyryl Sochacki 58c0fc396f i18n(pl): translate 1155 missing UI messages (#9102)
Everything en.json gained since the last catch-up except the render.*
messages: Preferences, navigation bar, assistant settings, materials,
hole presets, joint move, edit in place, point clouds, view angles,
tool help texts and error hints. Added as one pending locale file.
2026-10-30 17:19:37 +00:00
GavinSchmidt 0952686c1b Sheet metal: personal Sheet Metal Rules, saved in the profile and offered by every design's rule manager (#9346) (#9520)
* Sheet metal: personal rules library in the profile, offered by every design's rule manager

Adds personal_sheet_rules.rs and its tests, the personal_sheet_rules preference, and the rule manager wiring.

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

* Sheet metal: pending strings for personal rules

English and the complete catalogs (ar, he, ja, vi, zh), plus de and ko.

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

* Docs: personal rules in the sheet metal guide

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

* Docs: glossary entry for personal sheet metal rule

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:12:12 +00:00
semmelrinde 64b506dccf Sketch: the coordinate readout starts right of the pointer arrow (#9480) (#9484)
* Sketch: the coordinate readout starts right of the pointer arrow (#9480)

The readout of a sketch tool's point (2D and 3D sketches) started 10
points below and right of the snapped point, under the system arrow,
which hangs about 12 points wide below the hot spot and covered the
first value. It now starts 18 points right and 10 below the pointer
(the point snaps away from it), from the snapped point only when there
is no pointer over the view.

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

* Sketch: the readout clears the next larger pointer too (#9480)

The reporter uses the system's pointer size 2, whose arrow is about
18 × 28 points: start the readout 26 points right of the hot spot
instead of 18, and test against that arrow.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:57:45 +00:00
semmelrinde 61195235d8 Layout: three more edge clamps give way in an inside-out view (#9504)
The clamp that crashed #9492 (f32::clamp between a view's edges, which
panics once the view comes out inside out) also sits in the room tool
windows get beside the docked Assistant, the Render selection bar's
anchor and a left-anchored panel's anchor. Use shell::within there,
the clamp that gives the lower edge for an empty range (#9271).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:57:38 +00:00
semmelrinde 6241decaf9 Galician: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9537)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:57:22 +00:00
semmelrinde 3288adf3dd Czech: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9525)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:50:24 +00:00
semmelrinde 5b53a1354a French: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9527)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:50:17 +00:00
semmelrinde 579dddaea0 Italian: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9529)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:50:10 +00:00
semmelrinde 9052b69e81 Dutch: translate the 31 messages main gained after batch 8, new Preferences included (batch 9) (#9530)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:50:04 +00:00
semmelrinde 2c5d875ab7 Swedish: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9532)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:57 +00:00
semmelrinde 04efc68578 Turkish: translate the 32 messages main gained after batch 8, new Preferences included (batch 9) (#9533)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:51 +00:00
semmelrinde 0e137b947f Traditional Chinese: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9534)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:44 +00:00
github-actions[bot] 97a24941d2 Regenerate generated files and fold pending files [skip ci] 2026-10-30 17:34:50 +00:00
github-actions[bot] c2a19bf375 Regenerate generated files and fold pending files [skip ci] 2026-10-30 17:27:38 +00:00
semmelrinde e40d7aaee7 Indonesian: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9536)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:30 +00:00
semmelrinde 2b18b68307 Norwegian Bokmål: translate the 35 messages main gained after batch 8, new Preferences included (batch 9) (#9538)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:23 +00:00
semmelrinde b9ae8814d4 Bosnian: translate the 14 messages main gained after batch 8, new Preferences included (batch 9) (#9539)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:16 +00:00
semmelrinde f3a4c62240 Serbian: translate the 14 messages main gained after batch 8, new Preferences included (batch 9) (#9541)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:10 +00:00
semmelrinde dbf38d84ef Russian: translate the 14 messages main gained after batch 8, new Preferences included (batch 9) (#9542)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:49:03 +00:00
github-actions[bot] f913a2a459 Regenerate generated files and fold pending files [skip ci] 2026-10-30 17:00:07 +00:00
Nathan Dane f504c82216 Viewport: integrate Section status with current UI changes (#8548) (#8557) 2026-10-30 16:23:37 +00:00
SilentWolf75 df18823d0a Start: tell same-named recent designs apart (#9081) (#9174)
* claim: #9081

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tell same-named recent designs apart (#9081)

Show the file name with extension, ellipsize long paths in the middle so the end stays visible, and label imports by kind (STEP, STL, …) instead of always Design.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Start: Recent paths take their ink from the theme, and the middle-ellipsis helper is exported for tests only

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:21:26 +00:00
austin62jake 0396b2d00a Model cache: a design with mesh bodies keeps its saved model (#9170) (#9218)
* claim: #9170

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

* Model cache: a design with mesh bodies keeps its saved model (#9170)

One mesh body (an inserted STL or 3MF, or a STEP part shown as a mesh of
its faces) left the model cache out for the whole design, so opening it
evaluated every feature again. The doc records refused mesh bodies, and
the native B-rep codec the io cache stores bodies with refuses them too.

Kernel work is paused, so the kernel is left alone: the io cache stores a
mesh body as its own slice kind (SCMESH01, a JSON header with the counts,
colour and paint, then the vertices as f64 and triangles as u32), read
back through mesh_body, with_color and with_paint. A mesh body is kept
only when its slice reads back as the same vertices bit for bit, the same
triangles, colour and paint. The decoder checks the counts against the
bytes before allocating. The picture shown while opening draws a mesh
body with its own triangles (native_preview_mesh refuses mesh bodies).

Cache version 4 -> 5; docs/design-container.md describes the mesh slice.

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

* Model cache: decode mesh slices with as_chunks, as clippy asks

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-30 16:17:27 +00:00
AsTechEvolves b1c132bc9a Assistant: drop earlier thinking blocks when history is trimmed, so Claude requests stop failing (#9417) (#9448)
* claim: #9417

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

* Assistant: drop earlier thinking blocks when history is trimmed (#9417)

Claude signs each thinking block against the conversation before it. Once
drop_old_views swaps an older view for a note, or trim_history drops the
oldest messages, every later block stops matching and the API refuses the
request with HTTP 400 "Invalid signature in thinking block". For the
Anthropic protocol, a trim now also removes the thinking and
redacted_thinking blocks from earlier replies, which the API allows. A
reply that was only thinking keeps a short note. Requests where nothing
was trimmed are sent unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:13:35 +00:00
github-actions[bot] 71b3a70254 Regenerate generated files and fold pending files [skip ci] 2026-10-30 16:24:57 +00:00
github-actions[bot] bcdecd19f4 Regenerate generated files and fold pending files [skip ci] 2026-10-30 16:17:21 +00:00
austin62jake 3b3f860adc Sketch: Project takes a construction axis, not only X, Y and Z (#9324) (#9471)
* claim: #9324

* Sketch: Project takes a construction axis, not only X, Y and Z (#9324)

sketch.project with {"axis": "Axis1"} upper-cased the name, and the axis link knew only the
origin axes, so a construction axis failed with "unknown axis `AXIS1`". X, Y and Z (any case)
are still the origin axes; any other name must be a construction axis, kept as its feature names
it. The link finds the axis where the timeline before the sketch puts it (construction_axis_in,
as Revolve does) and projects it as an origin axis: its line in the sketch plane, or a point
when the axis stands square to it. Renaming the axis renames the link, as for a construction
plane's projection.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 15:51:57 +00:00
PaddleStroke f0d5666a74 Closing: stop the Home picture builds instead of waiting for every cold recent file (#8398) (#9416)
* claim: #8398

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

* Closing: stop the Home picture builds instead of waiting for them (#8398)

Dropping SolveApp waited up to 10 s for the Home picture threads to end
(#6063). The thumbnails thread imports and rebuilds every cold recent
file (STEP and mesh imports, designs without a stored picture) one after
another, and its channel only closes once all are done, so closing the
window froze until every cold recent was built or the 10 s ran out.

The picture threads now build under a shared progress watch that
finish_pictures cancels first: the design being built stops at its next
feature (a STEP import at its next step), the remaining cold files are
skipped, and a build stopped part way draws and caches no picture.
Pictures already on the GPU are still waited for, so the driver crash
#6063 fixed stays fixed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:46:06 +00:00
Kai Evans f1c2f67d63 Quick measurement: the selection's length, area or distance in the view's corner (#7000) (#7513)
* Quick measurement: the selection's length, area or distance in the view's corner (#7000)

With edges, faces, sketch curves or points selected, the view's bottom right corner above History
shows what they measure, as the Measure tool lists it: an edge's length (Ø and R for a round
one), a face's area, a point's position; two items' distance and angle; several edges' or faces'
count and total. It is computed off the window thread from a copy of the session whenever the
selection or the design changes, and a click copies it. Preferences > Interface turns it off. The
Measure dialog's rows come from the same function.

* Quick measurement: off by default, turned on under Preferences > Interface (#7000)

The quick_measure preference now defaults to false, so nothing changes until it is turned on.
Its tests turn it on, and a new test checks that nothing shows until it is.
2026-10-30 15:46:01 +00:00
Homero Thompson 32900556e0 Control server: a subscribed connection is told of design changes (#8315) (#8333)
* claim: #8315

* fix(control-server): release the watch seat deterministically (#8315)

The seat a subscribed connection holds on the DesignWatch lived in the
events thread alone, so it left the watch only when that thread ended:
on the first write a closed client refuses, which needs the design to
move, or when the thread noticed the end of the stream, which waits for
the connection's own thread to come out of its blocking read. A closed
connection could therefore keep counting on the watch for a second or
more, and the test that asks for the count to be gone could lose its
race with the ten seconds it waits.

The seat is now shared: it leaves the watch once, whichever thread sees
the connection's end first, the connection's own thread at the end of
the stream or the events thread at the first refused write. The count
of subscribed connections is right as soon as the connection is gone,
and the events a subscribed connection is told of do not change.
2026-10-30 15:45:51 +00:00
Frank bf14bf3678 Inspect Sketch: clicking it shows a Sketch report window (#6262) (#6263)
* claim: #6262

* Inspect Sketch: the ribbon button opens a Sketch report window (#6262)

sketch.inspect gets a place on the Sketch tab (Inspect panel). ui.start opens a
read-only Sketch report: solver status, degrees of freedom left, counts of
curves, points, constraints and profiles, the curves and points that are not
fully constrained, and Copy JSON. engine.execute still only returns the JSON.

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

* Inspect Sketch: a Sketch Report button on the Sketch tab while a sketch is open (#6262)

SKETCH/INSPECT placed it in the Inspect tab's Surface Quality drop-down, which
is hidden while a sketch is edited. It now has its own Inspect panel on the
Sketch tab, labelled Sketch Report; the test clicks the ribbon button.

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

* Sketch Report: strings in a pending locale file, help for sketch.inspect (#6262)

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

* Sketch Report: the Sketch tab fits at 1366 px; the width test closes the report (#6262)

The panels left of the Finish group keep the gap before it when they are fitted,
so they collapse before they reach it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Frank <plexi32@gmail.com>
2026-10-30 15:39:25 +00:00
DuBearte f0a5fc5016 Open: a DWG file says what it is and what to do instead (#9328) (#9487)
Opening a DWG stopped with "design file is not UTF-8 JSON". It is told
apart by its signature, whatever the file is called, and the error points
to STEP or DXF.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:18:40 +00:00
github-actions[bot] ad1652683b Regenerate generated files and fold pending files [skip ci] 2026-10-30 15:48:25 +00:00
github-actions[bot] 0d778e074b Regenerate generated files and fold pending files [skip ci] 2026-10-30 15:39:35 +00:00
PaddleStroke 0b2d51d301 Sketch: a projected face follows a resize that moves its corners far (#9001) (#9453)
* claim: #9001

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

* Sketch: a projected face follows a resize that moves its corners far (#9001)

A sketch on the face of an angle kept its projected face boundary (and
reported "projected face boundary identity is ambiguous") when the wall
thickness changed: the inner corner moved by more than a tenth of the face,
past the nearest-point match, and the same-order fallback refused faces.

Accept the same-order layout when, for every point, the point read in that
order is also clearly the nearest one on the same loop with lines the same
way. Rims and holes, and rotated readings of a rectangle, stay refused.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:05:36 +00:00
Frank bf7979d4a7 Insert Part: seat a fastener in several holes at once (#9341) (#9342)
* claim: #9341

* Insert Part: seat a fastener in several holes at once (#9341)

parts.insert / parts.fastener take `at` as one pick or a list of picks.
The part is built once as one component; each hole gets its own
placement and rigid seat joint. All placements are made before any is
seated, since joint.create takes the part's seating point where the
placement is. The insert now runs on a scratch session like
parts.resize, so it is one undo step, and a pick off every body fails
it before anything is made.

The Insert Part dialog's hole input is multi-select ("Holes
(optional)") and OK passes every pick.

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

* Insert Part: picks on one hole's rim seat one part (#9341)

A hole's rim is drawn as two half-circle edges, and one click on it
picks both, so four clicks gave eight picks and two screws per hole.
Picks whose wall point lies level on the same hole axis, on the same
body placement, are one hole.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:37:02 +00:00
DuBearte 5439bfc0b5 Expressions: a decimal comma inside plain parentheses (#9408) (#9447)
A comma separates arguments only directly in a function call, so plain
parentheses take a decimal comma like the rest of an expression:
`-(width-0,5)/2`. The UI no longer rewrites commas before wrapping a value
in the design unit's parentheses.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:34:56 +00:00
austin62jake c2ea096abb Assistant on Windows: find Node in its install folder when the launcher's PATH lacks it (#9297) (#9458)
* claim: #9297

* Assistant on Windows: find Node in its install folder when the launcher's PATH lacks it (#9297)

An npm-installed agent starts on Windows through its .cmd shim, which runs `node`, with the PATH
search_dirs makes: SolidCraft's own PATH, then npm's and other installers' folders. Node's own
install folder was not among them, so started from a launcher that leaves it off the PATH (the
ArtCraft launcher), the agent stopped with "'node' is not recognized". The search now also takes
%ProgramFiles%\nodejs, %ProgramFiles(x86)%\nodejs and nvm-windows' %NVM_SYMLINK%, after
everything it took before. search_dirs reads its variables through search_dirs_in, so a test can
give it a launcher's environment.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 14:27:10 +00:00
Colin Faulkingham 085853315d Sketch: copy completed sketches between designs (#5427) (#8962)
* claim: #5427

* Sketch: paste completed sketches across design tabs
2026-10-30 14:09:37 +00:00
Mario Geiger 332f96c26a Point clouds: colour and filter by any property the file carries (#8816) (#8819)
* Point clouds: colour and filter by any property of a PLY file, on range tracks (#5151)

A PLY file's other vertex properties are kept as named numbers on the
points (whole-numbered for integer types; a comment `property: 0 name,
1 name` names a label's values). A cloud is coloured by any of them, a
ramp or a palette for whole numbers, and filtered by up to eight, each a
range or a set of named values. The GPU colours and filters the points
itself, so editing a range uploads nothing.

In the Edit Point Cloud card each range is a track, as in an audio
editor: a histogram of the property over what the other filters keep,
stacked by colour, under an overview strip. Ranges are drawn, moved,
snapped to colour changes, typed (Start, End, Length, relative values)
and played (Space or Play). The automatic colour range is exactly the
values the property takes over what the filters keep.

pointcloud.insert and pointcloud.edit take color_by: <property>,
color_range and filters; pointcloud.list reports them with the
properties. docs/point-clouds.md describes it all.

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

* Point clouds: note the saved properties, colouring and filters as a pending format change (#8816)

A design now saves a cloud's per-point columns and their value names, colouring by a property and
filters; an older release must refuse it instead of dropping them.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:02:29 +00:00
PaddleStroke 56e752c182 Toasts: a close (×) with a countdown ring instead of a Close button; a shorter 3D mouse driver label (#7971) (#8763)
* claim: #7971

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

* Toasts: a close (×) with a countdown ring instead of a Close button (#7971)

Every toast the user may close (the status, the 3D mouse offer and the
stand-up note) has a close (×) at its top right, handle toast:<name>:close,
in place of a Close button; it does what the button did, is a kit-drawn
button with an accessible name and hover text, reachable by Tab, and dead
behind an issue report. Toasts that go by themselves draw a thin ring
around it that shrinks over their time; pointing at the offer or the note
starts their time again, as before, so their ring is full while pointed at.

The 3D mouse offer keeps three buttons, Calibrate…, Use Direct Driver (the
detail in its tooltip) and Never show again, on one row: the corner
toasts are 400 points wide, as the row's estimated widths need 356.

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

* Toasts: lay buttons out at their drawn widths; corner toasts 360 wide (#7971)

The toast's button rows used an estimate of each button's width that ran
about 40 points over the kit button as drawn, which wrapped the 3D mouse
offer's three buttons and left the main action short of the right edge.
Each button's drawn width is now remembered and the rows laid out with it
(the estimate stands in for the one frame before a button was first
drawn), so the main action is flush right; the corner width is the
smallest round one that fits the offer's three buttons in a row: 360.

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

* Toasts: keep the analysis status test whole after rebasing onto main

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-30 13:48:29 +00:00
github-actions[bot] 014ccf5ff4 Regenerate generated files and fold pending files [skip ci] 2026-10-30 15:12:22 +00:00
github-actions[bot] ab309f2219 Regenerate generated files and fold pending files [skip ci] 2026-10-30 15:05:46 +00:00
TaeHeon Kim 6c9afc6f7e Start the window on OCLP Macs with SYSTEM_VERSION_COMPAT=1 (#3080) (#8930)
On Macs patched with OpenCore Legacy Patcher the OS reports a newer
macOS version than the GPU's Metal compiler supports, so wgpu picks a
shader standard the compiler refuses (`invalid value 'metal3.2'`) and
the window never draws.

The variable has to be set before the process starts, and Rust 2024
makes `set_var` unsafe (this crate denies unsafe code), so main()
now re-launches the binary with SYSTEM_VERSION_COMPAT=1 when the OCLP
NVRAM key (OCLP-Version) is present, and exits with the child's status.
A process that already has the variable set never re-launches, so
there is no loop and a user's own value is respected. If the check or
the spawn fails, startup continues normally.

Tested on an OCLP Mac: one re-launch message, graphics render
correctly; with SYSTEM_VERSION_COMPAT=1 set beforehand it does not
re-launch.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:48:19 +00:00
SvenCarstensen aff8106205 Help: check for updates and show results in the selected language (#8896)
* Help: add manual update checks and status window

* Test manual update checks and localized results

* Add update check to the native Help menu

* Localize Help update checks in all app languages

* Document manual update checks from Help

* Document ui.help updates control action
2026-10-30 13:41:00 +00:00
semmelrinde 8ced905843 macOS: quitting on a Mac with a Touch Bar no longer crashes (#7535) (#7762)
* claim: #7535

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

* Vendor accesskit_macos 0.26.3 as published (#7535)

The published package, unchanged, so the next commit shows the fix as a diff.

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

* macOS: quitting on a Mac with a Touch Bar no longer crashes (#7535)

eframe's accessibility adapter (accesskit_macos 0.26.3) swaps the window's
view to a subclass of its own. When AppKit's Touch Bar observes the first
responder (nextResponder), key-value observing subclasses the view again.
Dropping the adapter on quit reset the view's class, which threw away KVO's
subclass, so AppKit's removal of its observer raised NSRangeException ("not
registered as an observer") and the app ended with EXC_BREAKPOINT.

accesskit_macos 0.27.2 fixes this (AccessKit/accesskit#835), but egui 0.36
is on accesskit 0.24, so vendor 0.26.3 with 0.27.2's src/subclass.rs and its
tests: the adapter leaves the class alone when KVO subclassed the view.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:35:37 +00:00
SilentWolf75 21340ba019 Web: Chinese UI loads a Chinese font (#8894) (#8981)
* claim: #8894

Co-authored-by: Cursor <cursoragent@cursor.com>

* Web: load Simplified/Traditional Chinese fonts for zh and zh-TW (#8894)

BIZ UDPGothic only covers Japanese, so Chinese UI showed tofu. Ship locale-glyph Craft UI SC/TC subsets and fetch the matching face on demand.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 13:33:23 +00:00
Colin Faulkingham 8b25fa0ae4 Configurations: reject invalid parameter expressions when saving rows (#9088) (#9414)
* claim: #9088

* Configurations: reject invalid row expressions (#9088)
2026-10-30 13:32:50 +00:00
ReDead c0b0369aa2 Sketch: hide constraint glyphs when zoomed out (#8980) (#9023)
* claim: #8980

Hide constraint glyphs when zoomed out (claim).

Co-authored-by: ReDead <ReDead@users.noreply.github.com>

* Sketch: hide constraint glyphs when zoomed out (#8980)

Fixed-size constraint glyphs stayed visible at every zoom and cluttered the
view when far out. Hide them below a sketch-plane scale threshold, with the
old always-on behaviour behind hide_constraint_glyphs_zoomed_out.

Co-authored-by: ReDead <ReDead@users.noreply.github.com>

* i18n: complete-catalog strings for zoom-hide preference (#8980)

Add ar/he/vi translations for prefs.hide_constraint_glyphs_zoomed_out
(and its note) so cargo xtask i18n --check no longer reports 6 gaps in
complete catalogs.

Co-authored-by: ReDead <ReDead@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: ReDead <ReDead@users.noreply.github.com>
2026-10-30 13:31:09 +00:00
Frank 44a9f9ee42 GPU path tracer: the path shader compiles on NVIDIA's Vulkan driver (#6705) (#6850)
Native compilers inline every call, and `main` inlined 33 BVH walks: three
sample_path calls, and a shadow walk per light source family. NVIDIA's
compiler (driver 610.88) crashed in nvgpucomp64.dll or ran out of memory.

- main settles the adaptive budget first, then samples at one call site.
- direct_light takes over direct_studio, visible and the fixed-light loop:
  one loop over the sources with one shadow-ray call site, drawing random
  numbers in the same order.
- next_vertex (a select) replaces (a+1u)%3u: after a BVH walk that index
  crashed NVIDIA's compiler within seconds.

main now inlines 5 BVH walks; main_inlines_few_bvh_walks counts them with
naga, so the limit holds without an NVIDIA GPU.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:28:57 +00:00
kaltenbrunnermarkus f331a0740b Model cache: write each set of sketch keys once, and build the states on threads (#9010)
Problem: every state of a saved model wrote the evaluation key of each sketch before it, so a
design of many sketches and many features after them repeated the same map over and over. In
the SELENE rover (5,484 features, 2,116 bodies) that was 27 of the 33.5 MB manifest: the
manifest hit its 32 MiB limit, the save thinned the cache (#3220) to 2,140 of 5,484 states, and
every open evaluated the 3,344 left-out features again. Reading the kept states one after
another took a few seconds more.

Solution: as for sheet bodies (#6152), the records keep a list of sketch-key sets, each distinct
set written once, as the keys it sets and drops relative to the set written before it; a state
names its set by place. A thinned model keeps the whole list and counts it once against its
budget. The reader counts every set's keys, and each state's, towards MAX_HELD before it copies
them (a short record can stand for a large set), then builds the states on threads
(kernel::map_on_threads) once the shared records are read. The cache format is now /5: an older
cache is a miss and the design is evaluated.

SELENE's manifest is 15.9 MB with every state kept; opening it evaluates nothing. Parse and
evaluate on opening, release build, median of 5 on a shared machine: 7.36 s -> 1.14 s. The
evaluated bodies are identical (inspect.measure of all 2,116 bodies).

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

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
2026-10-30 13:28:16 +00:00
fkirill 12636b67be Select hidden geometry with Select Other and fit the list to the window (#5574) (#5577)
* claim: #5574

* Pick hidden geometry near long clicks in a five-row list (#5574)

* fix: make Select Other configurable and fit its list to the window

---------

Co-authored-by: Kirill Frolov <k.a.frolov@vk.team>
2026-10-30 13:25:56 +00:00
semmelrinde 9f5ac92faa Saved model cache: keep fast opening after an update (#7168) (#7354)
* claim: #7168

* Saved model cache: keep fast opening after an update (#7168)

The model cache was read only by the exact build that wrote it (same app
version and commit), so every update evaluated every design again on its
first open. It is now keyed by the evaluation source instead: a digest of
the committed source the model is computed from (doc and the workspace
crates it is built from, the patched truck crates, locked external
dependencies, workspace build settings without the version, and the
cache reader), with the platform. An update that changed none of that
opens designs from their cache; one that changed any of it evaluates
them again, as before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:25:42 +00:00
PaddleStroke 6e39efd4ac Theme: System follows the desktop's dark/light setting on Linux GNOME (#7944) (#8123)
* claim: #7944

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

* Theme: System follows the desktop's dark/light setting on Linux (#7944)

On GNOME (Wayland, or X11 through XWayland) the window toolkit never reports
the desktop's dark or light preference, nor its changes, so Theme: System kept
its colours. The Linux app now reads org.freedesktop.appearance color-scheme
through the XDG Desktop Portal's Settings interface (then GNOME's color-scheme
and gtk-theme when it has no preference) on a thread of its own, follows the
portal's SettingChanged signal, and falls back to gsettings or a slow poll.
The window reads the answer through Services::desktop_dark; the decision from
the raw values is prefs::desktop_prefers_dark. Windows and macOS keep the
window toolkit's theme.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:25:23 +00:00
C.Vengels 3c40edd12a Print face: Send to Slicer lays a body on its marked face (#8429) (#8433)
* claim: #8429

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

* Print face: Send to Slicer lays a body on its marked face (#8429)

A body can have one flat print face (Document::print_faces, held by the
certified target a sketch on a face holds). body.print_face {body, point?}
marks or clears it. Send to Slicer, Save Mesh with a slicer and EasyPrint
turn and move the exported copy so the face lies on the bed at Z = 0 with
the centre of mass over the origin; a face that can't be found is a
warning and that body is exported as it lies. Move/Copy and Mirror give a
copy its source's print face. One pending format file; the command
has its own icon (print_face).

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

* Print face: Ctrl+B, the face menu and the Send to Slicer row (#8429)

Ctrl+B and the face context menu mark the selected flat face as its
body's print face, or clear it. Send to Slicer shows the print face of
the one body being sent in a Print face input, takes a flat face picked
in the viewport and clears it when emptied.

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

* Print face: a light tint in the viewport and its strength in Preferences (#8429)

Each visible body's print face is tinted towards the theme's section
colour and outlined, at the strength set under Preferences > General >
Slicer (print_face_highlight, 0 to 1, 20 % by default; 0 hides it). The
user guide describes the print face, and the known limitation after a
shell of a shelled body is listed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:25:18 +00:00
github-actions[bot] 81b620d7aa Regenerate generated files and fold pending files [skip ci] 2026-10-30 14:36:14 +00:00
SixSolid 9af2ae9a0f Agent chat window: an off-screen chat window no longer stalls the main window (#9118) (#9119)
* claim: #9118

* Agent chat window: an off-screen chat window no longer stalls the main window (#9118)

The chat popped out into its own window is an immediate viewport: eframe draws
and presents it inside the main window's frame. Both surfaces waited for vsync,
and a Wayland compositor sends no frame callbacks to a window it isn't showing,
so the main window stalled behind the chat window's present.

A surface other than the main window's is now configured with Mailbox, else
Immediate, when the configured mode waits for vsync and the surface offers one
(viewport_present_mode in the vendored egui-wgpu). The main window keeps vsync
and still paces both.
2026-10-30 13:24:30 +00:00
Kai Evans 1754318df7 Browser: the visibility eye at the right end of each row (#8718) (#8720)
* claim: #8718

* Browser: the eye sits in a column at each row's right end (#8718)

Tree rows draw the visibility eye right-aligned at the row's end, clear of
the fold arrow and the radio and of a floating scroll bar; names stop short
of it. Preferences > Interface > Eye next to the fold arrow
(browser_eye_left) keeps the old position. Rows report their control
positions so placement handles and inline rename follow the layout.

* Docs: the browser's eye column and browser_eye_left (#8718)

* Browser: the eye's tip shows by the eye at the row's end (#8718)

* Browser: a gap keeps the fold arrow and the radio apart; the eye clears a floating scroll bar (#8718)

The eye's old slot leaves a 10 pt gap instead of 4 pt, so the radio's hit
area is 7.5 pt from the fold arrow's and History pattern rows step in from
their sketch as far as browser children do. The eye's inset follows the
scroll style: only a floating bar laid over the rows takes room. Rows
publish indent:<label> where their depth puts them.

* Tests: browser depth checks run on the default layout, by indent handles (#8718)
2026-10-30 13:24:22 +00:00
semmelrinde f273195144 Czech: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9427)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:55 +00:00
semmelrinde 247a77e1b1 French: translate the 25 messages main gained after batch 7, construction lines included (batch 8) (#9428)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:48 +00:00
semmelrinde ce4212a33a Hungarian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9429)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:41 +00:00
semmelrinde 522bd95254 Italian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9430)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:34 +00:00
semmelrinde b0b1e6832e Dutch: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9431)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:27 +00:00
semmelrinde 3cb9aa07b5 Romanian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9433)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:19 +00:00
semmelrinde 60f6836ab7 Swedish: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9434)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:12 +00:00
github-actions[bot] 43f0438fbb Regenerate generated files and fold pending files [skip ci] 2026-10-30 13:58:26 +00:00
github-actions[bot] ded10b52da Regenerate generated files and fold pending files [skip ci] 2026-10-30 13:49:40 +00:00
Frank 4bc5bdc791 Reverse zoom direction no longer flips 3D mouse zoom (#8993) (#8994)
* claim: #8993

* Reverse zoom direction no longer flips 3D mouse zoom (#8993)

The checkbox sits under Wheel and trackpad and now reverses the wheel
(and the navigation bridge's wheel-style steps) only. The 3D mouse cap
zooms by its own per-axis invert switch alone: device_camera no longer
takes a reverse_zoom argument, nor does the test model in Preferences.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:12 +00:00
semmelrinde 1baaebd3ef Turkish: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9436)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:23:05 +00:00
semmelrinde 97e467f2d1 Traditional Chinese: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9437)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:58 +00:00
semmelrinde 665ff84495 Malay: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9438)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:52 +00:00
semmelrinde 07b3b92703 Indonesian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9439)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:45 +00:00
semmelrinde ddf56e1246 Galician: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9440)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:38 +00:00
semmelrinde 0ddbe87935 Norwegian Bokmål: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9441)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:31 +00:00
semmelrinde 19dc8b6881 Bosnian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9442)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:25 +00:00
semmelrinde 3e2e229dd1 Croatian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9443)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:18 +00:00
semmelrinde c8d9a4bfdd Serbian: translate the 29 messages main gained after batch 7, construction lines included (batch 8) (#9444)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:22:12 +00:00
Dane Doubell 6c764dce06 Sketch: pick a whole profile or chain for Mirror/Move/Copy/Scale entities (#8390) (#8812)
* claim: #8390

* Sketch modify: pick a whole profile or chain for the shared Entities input (#8390)

- Click inside a closed profile selects its whole boundary (and its holes);
  double-click a curve selects its whole chain, for Mirror, Move/Copy, Scale
  and the patterns. Single curves and points keep working as before.
- The mirror line may itself be among the entities: the command leaves the
  line in place and mirrors the rest, instead of rejecting it.

* Mirror: the shared line case is valid, not an error (#8390)

Update the half-filled-window test: a mirror line that is also among the
entities is now accepted (the command leaves it in place and mirrors the
rest), matching the engine.

---------

Co-authored-by: Dane Doubell <2853036+danedoubell@users.noreply.github.com>
2026-10-30 13:13:53 +00:00
SilentWolf75 655e771455 Update: Cancel stops the download sooner (#8908) (#8946)
* claim: #8908

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update Cancel clears the UI and stops before checksum downloads.

Drop the install job as soon as Cancel is pressed so Downloading ends
immediately, and check the cancel flag around feed refresh and
checksum GETs so a stall before the package body no longer ignores it.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 13:13:44 +00:00
bherbruck 15f74b7981 Agent chat composer: one margin, input box fills its area; hint stays on one line (#7268)
* Assistant composer: the input box fills its area, and the hint stays on one line

The composer footer inset the box by its own margin on top of the box's text
padding. The footer now has no margin, so the box spans it on all four sides and
only the text padding remains. The hint is painted on a single row, cut with an
ellipsis when the panel is narrow.

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

* Assistant composer: the hint takes its colour from the theme, not a literal

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:47:04 +00:00
github-actions[bot] 9b607f6f1f Regenerate generated files and fold pending files [skip ci] 2026-10-30 12:38:49 +00:00
github-actions[bot] ab67126eec Regenerate generated files and fold pending files [skip ci] 2026-10-30 12:31:20 +00:00
Nathan Dane 3b8cbc4e22 Viewport: surface analysis status items and legend keys (#8547) (#8556) 2026-10-30 12:12:19 +00:00
Frank f59f6698f1 Ribbon drop-downs as a grid of large icons, as an option (#4739) (#5012)
* Ribbon drop-downs as a grid of large icons, as an option (#4739)

Preferences > Interface gets two settings, both off by default:

- Menus: List | Large icons. With Large icons a panel's drop-down shows its
  commands as rows of up to five icons at 32 px (twice the list's), keeping
  the list's group headings and order. The name, shortcut and help are in
  the tooltip, as on a ribbon button. Arrow keys move across and between
  rows; a click starts the command; right-click and drag pin it as before.
  Resting on an icon for two seconds arms a pin in its corner; a quicker
  click anywhere starts the command, so nothing is pinned by accident. A
  pinned command shows its pin.
- "Leave pinned commands out of the drop-down menus": a drop-down (list or
  grid) leaves out the commands the ribbon shows as buttons; a panel too
  narrow for its buttons lists them again, and a menu is never left empty.

No icon artwork or colours change. Sketch > Create keeps its family list.

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

* Sketch > Create in the large-icon grid: every variant under its family (#4739)

With Menus: Large icons, Sketch > Create showed its family sub-menus as
plain text rows (Line, Rectangle, Circle, ...), unlike every other menu. It
now shows every variant as a large icon under its family's heading, then the
other tools under "More". The list keeps its family sub-menus.

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

* Large-icon menus: groups side by side, no footer (#4739)

A menu of two or more headed groups (Sketch > Create's families, Model >
Modify's Edges, Faces, Bodies, Specialise) puts the groups side by side, each
heading over a column of its icons, at most two across. Menus without groups,
or one too wide for the window, keep rows of up to five. The pinning note
under the large-icon menus goes: the tooltips and right-click menus say it.

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

* Large-icon menus: a separator line between groups side by side (#4739)

As between the ribbon's sections: a 1 px line in the theme's border colour.

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

* Large-icon menus: at most three rows, growing to the right (#4739)

A menu fills five columns, more when that would take over three rows (Model >
Create: 7 x 3); in grouped menus a group too big for three rows gets more
columns. Only a window too narrow for that wraps into more rows. A test checks
every panel's menu of every tab.

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

* Large-icon menus: group headings centred over their icons (#4739)

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

* Drop-down group headings: smaller than the drop-down's header, not in capitals (#4739)

Edges, Faces, Bodies (and Sketch > Create's families) were drawn in capitals
at font_small, which reads larger than the drop-down's own header
(font_caption). They now use font_micro, written as named, in the list and the
large-icon grid. Review feedback on #5012.

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

* Large-icon menus: start a command from the grid at its icon, as main's rows do (#4739)

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

* Ribbon icon grid strings: a pending locale file, with ar, he and vi (#4739)

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

* Large-icon menus: commands in no group stand apart, so a left-out group doesn't move them (#4739)

With Convert to Sheet Metal pinned and left out of Model > Modify, its
Specialise group went and Delete, in no group, moved under Bodies.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:59:08 +00:00
Rick Morgans bd1525dcfb Expressions: hyperbolic functions sinh, cosh, tanh and their inverses (#9332) (#9334)
* claim: #9332

* Expressions: hyperbolic functions sinh, cosh, tanh and their inverses (#9332)

`sinh`, `cosh`, `tanh` and `asinh` take a unit-less number and give one,
as `exp` does; `acosh` refuses x < 1 and `atanh` refuses |x| >= 1, worded
as `asin` and `ln` refuse what lies outside their domain. They are in the
expression help (`FUNCTIONS`, the parameter autocomplete), the
hostile-expression fuzz tokens, the parameters and equation-curve guides,
and the checking skill's list of functions. Horn flares (Salmon's hypex
`cosh h + T sinh h`), catenaries and the tractrix can now be written as
they are stated instead of expanded into `exp`.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:13:59 +00:00
kaltenbrunnermarkus 1e5eb1e3b8 Sketch: find profiles island by island, so dragging in a large imported logo stays smooth (#6896) (#9313)
* claim: #6896

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

* Sketch: find profiles island by island and say when the search is cut short (#6896)

Every live drag frame ran the profile search over the whole sketch: every
spline piece of a large imported logo was sampled, checked for crossings
and traced again, about 100 ms per frame at 6,000 degrees of freedom.

Profile curves are now split into islands whose enlarged piece boxes do
not touch (the margin exceeds every tolerance the search uses), each
island's loops are found on their own and cached by a fingerprint of its
geometry, and the loops are nested into profiles over the whole sketch in
the order a single search finds them, so profiles, ids, holes and
whole-boundary certificates stay the same. The end-near-crossing lookup
covers its whole reach, independent of the grid's cell size.

The 50,000-piece limit now holds per island, and a sketch that still
exceeds it gets a warning instead of silently missing profiles.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:13:52 +00:00
ABDULLAH a1483d63c1 Arabic: plural wording in the Discard autosaves title (#9354)
recovery.discard_all_title read "discard {count} of the autosave"; it now names the autosaves as a plural.
2026-10-30 11:10:28 +00:00
semmelrinde 491687d4c1 Sketch: the cursor snaps On a spline, ellipse or conic while drawing, as on a line or circle (#9321) (#9343)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:10:25 +00:00
Rick Morgans 66bd64cc5e Engine: one contract check for a call, used by the preflight, the bridge and the app (#9339)
`schema::issues(id, params)` looks up the command's typed contract and
lists what is wrong with the parameters (none without a contract or for
an unknown id). The batch preflight, the MCP bridge's pre-send check and
the app's check of a design-switching command each did that lookup
themselves; they now call it. No behaviour change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:21:06 +00:00
jeromehzl f9146cdd58 Hole: a Hole at sketch points hides its sketch as an Extrude does (#9082) (#9211)
sketch_consumed knew Extrude, Revolve and the sheet metal flanges but not
a Hole drilled at a sketch's points, so that sketch stayed drawn inside the
holes. A Multiple hole now counts as using its sketch; Edit Feature shows
its points while the dialog is open. Single holes keep their sketch shown,
since a hidden sketch offers no points to snap to.
2026-10-30 09:37:53 +00:00
github-actions[bot] 23e7ac767e Regenerate generated files and fold pending files [skip ci] 2026-10-30 09:58:59 +00:00
phanoob 3ac7a196c9 Recover unsaved design?: Discard all, with a confirmation (#9299) (#9300)
* claim: #9299

* Recover unsaved design?: Discard all, with a confirmation (#9299)

A Discard all button at the left of the window's footer, away from Later.
It asks first ("Discard 7 autosaves? This cannot be undone.", Discard all /
Cancel), then discards every listed autosave the way its own Discard button
does (doc.recovery_discard on each id). Cancel goes back to the list
unchanged. The button publishes the handle recovery:discard_all.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:26:04 +00:00
Logan Night 202e713cf1 Preferences: search finds settings in rows without a label (#9265) (#9318)
* claim: #9265

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

* Preferences: a setting in a row with no label is found by the words it shows (#9265)

A search hid a row by its label before its controls were drawn. Seven checkbox rows and two segmented rows on the Interface page have no label, so "status bar", "search results" and "reveal on hover" found nothing. A hidden row with no label now keeps the words of its controls, as a dropdown keeps its value, and is drawn when they match. Each such row has a key of its own, so its neighbours are not drawn with it.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-30 09:25:24 +00:00
N3MI-DG 63923da2ca Viewport: an opened design is framed at the home view, in the middle of the view (#9195) (#9204)
* claim: #9195

* Viewport: an opened design is framed at the home view, in the middle of the view (#9195)

A fit the viewport cannot frame yet is kept and asked again on the next frame instead
of being consumed and lost, so a design opened from the Home screen or by launching on
a file no longer comes up at the default camera, wherever its own coordinates put it.

Framing then centres the model on the view rather than on the free rectangle between
the floating controls: `Layout` carries `middle`, the view without the panels, computed
before the compass, the navigation bar and the OK/Cancel corner join the obstacles. The
model is still sized to `drawable`, so it stays clear of every control, but only a panel
moves it.

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

* Viewport: keep the centred fit clear of the controls, and frame --sample (#9195)

The fit framed the model into a rectangle the size of `drawable` centred on `middle`, which is
not inside `drawable`, so the model could sit under the compass, the navigation bar or the
render product frame. The model is now sized to `drawable` as before, and only the room left
over in it moves the model towards the middle of the view.

`s27_move_triad` turns the Z ring on its far side: the model now sits further right, and the
Move dialog that docks on the right afterwards covered the ring's old grab point.

A launch with `--sample` builds the design straight into the session and never asked for a
fit; it now comes up at the home view, framed, like an opened design.

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

* Viewport tests: the cursor orbit's pivot keeps the pixel it was picked on (#9195)

With the model now centred on the view, one of the test's perspective drag points lands within
the pick tolerance of the box's top edge, and the pick snaps the pivot onto that edge, a few
pixels from the cursor. The orbit keeps that point still, as it should; the test compared it
with the cursor instead of with where it started.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-30 09:14:18 +00:00
jeromehzl 7e22aed739 View ribbon: Visual style and Navigation tool offer their choices instead of an error (#8868) (#9127)
Both commands need a choice, but start_with_variant_memory only special-
cased the parameterless display toggles, so a click on the ribbon (or
command search, a shortcut, ui.start) ran them with {} and showed only
the engine's "style must be ..." / "tool must be ..." error.

They now open a short menu, the way Units does: under the ribbon row
that started it (a second click closes it), or in the middle of the view.
Visual style lists the four styles with the current one checked; the
Navigation tool lists Orbit, Pan and Zoom, and picking the latched one
releases it, as its navigation-bar button does. The four styles come
from one list shared with the navigation bar's menu.
2026-10-30 09:04:39 +00:00
jeromehzl 966b10e731 Sketch: a feature on a failed sketch names that sketch (#9075) (#9276)
A sketch that failed (a dimension set negative by a parameter) left every
feature built on it with "unknown sketch 1 (it must come earlier in the
timeline)", which points at the timeline order. The evaluation now notes
the features that failed earlier in the pass, and a feature whose input
sketch is one of them fails with "sketch `Sketch1` failed: <its error>".
That result is never cached, since the cache key holds neither the
sketch's name nor its error: a second failing value or a rename shows
up at once.
2026-10-30 09:03:46 +00:00
github-actions[bot] 99eda4cd1b Regenerate generated files and fold pending files [skip ci] 2026-10-30 09:24:07 +00:00
jeromehzl 47292bb2bd Preferences: the window covers the ribbon behind it (#9078) (#9243)
A page window (Preferences) filled its frame with
dialog_body.gamma_multiply(0.97), which also scales the alpha, so the
ribbon's labels showed through its title bar, search field and heading.
It now uses the opaque dialog_body, as every other window does; the
navigation bar keeps its own translucent resting fill.
2026-10-30 09:01:33 +00:00
semmelrinde 37dc878f31 Shell: test the interface scale raised in a narrow window, the crash #9271 fixed (#9315)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:58:15 +00:00
Igor Bronovskyi 7da02460d8 i18n(uk): add pending translations for strings added in upstream (#8977)
* i18n(uk): add pending translations for missing strings

* i18n(uk): add pending translations for new strings from upstream

* i18n(uk): improve pending translations

* i18n(uk): professional translation of all pending strings

* i18n(uk): finish the pending translations for 29 October

Every value in uk-sync-29oct.json is now Ukrainian except the seven symbols
and formulas that must stay unchanged: the modifier keys, HDR and the three
inertia tensors.

Terminology follows uk.json: spot light is "прожектор", directional light is
"спрямоване", body is "тіло", render is "рендеринг". Camera orientation keeps
тангаж, рискання and крен.

Removes history-origin-bodies.json, which was empty after its content moved
into this file.
2026-10-30 08:39:45 +00:00
P'tite Tête f73f4da665 fix: Find a folded folder's mark in one pass over a long history (#9294)
- worst_status searched the history and the results for each feature of a folded folder, the square of the history's length every frame
- A folder of 16 features or more now finds places and results in tables built once, first of an id winning as feature_index and Model::result do
- Frame on a 4,880-body design: 20.4 ms -> 7.9 ms (M4 Max, release, headless)
- Test a_folders_mark_is_the_worst_of_its_features_own

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-30 08:16:36 +00:00
github-actions[bot] ef5985f841 Regenerate generated files and fold pending files [skip ci] 2026-10-30 08:22:05 +00:00
Ahmad Khan 71bb4f7c98 Docs: a beginner's page for equation curves, with pictures and examples (#9282)
A new user guide page, Equation curves, under Sketching: the first curve step by step, Explicit and
Parametric explained with tables of rules to type (a line, a parabola, a wave, a circle, a spiral),
radians, parameters with the involute of a base circle, building a part (a closed flower curve
extruded), editing, and common mistakes. The technical details stay in a short section for scripts
and agents.

Seven pictures, rendered by `cargo xtask book` from new scripts in docs/book/scripts/equation-*.json
(gitignored output, attributed in attribution/pending/docs-equation-curves.md). The Sketching page
keeps a short summary and links to the new page.
2026-10-30 07:54:27 +00:00
Rick Morgans 92af4a21c8 Control channel: a per-request param_check level, applied and confirmed by the app (#9184) (#9185)
* claim: #9184

* Control channel: a request's own param_check level, applied and confirmed by the app (#9184)

`engine.execute`, `engine.batch` and `engine.script` take an optional
`param_check: "off"|"warn"|"strict"` in their params. The app runs the
request at that level through the same `run_checked` path and batch
preflight a headless session uses, puts its own level back, and every
reply says the level it ran at as `param_check` beside `ok`. A command
that switches designs keeps the app's level; any other word is refused.
Readings and exports answered on a copy off the window thread keep the
level too.

`control.capabilities`, answered at once on the control-server thread,
lists what the channel does beyond the methods every version has:
`{"param_check": ["off", "warn", "strict"]}`. An older app answers
"unknown method".

The MCP bridge asks it once per connection. When the app lists the
level, every call and batch goes with it and the app checks against its
own commands; otherwise the bridge checks before sending, as #9073 does.
At strict, a reply that doesn't say the app checked the call is an error,
and the next calls are checked by the bridge. Also: `add_param_warnings`
takes the reply by `&mut`, and `with_param_check` sits beside
`Remote::new`.

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

* Control channel: a design-switching command is checked at the request's level

Review fixes for #9184. `component.edit_in_place`, `component.finish_edit`,
`view.next_tab` and `view.previous_tab` ran at the app's own level and
echoed `off`, so a misspelt call went through at strict. They now have
their parameters checked at the request's level before they run (and
still run at the app's own level, as each design keeps its own), and the
reply echoes the requested level. The MCP bridge at strict now requires
the echo to match its level, not just to be there. `param_check` is read
only by `engine.execute`, `engine.batch` and `engine.script`; other
methods ignore it. The off-thread test now shows the design change
computing off the window thread before it is refused.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:24:41 +00:00
ABDULLAH 5d6c5bab39 Arabic: sketch and spline terms in the construction line help and hints (#9269) 2026-10-30 07:05:00 +00:00
semmelrinde 8251be9c06 Shell: content kept outside a changed viewport no longer turns inside out and crashes the Render frame overlay (#9271)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:01:16 +00:00
Steve Jason 3d744cc8a2 A formula that is a bare number gets the design unit (#9261) 2026-10-30 06:42:04 +00:00
github-actions[bot] 1447bc772a Regenerate generated files and fold pending files [skip ci] 2026-10-30 06:56:11 +00:00
semmelrinde 7b24a1dc1e Sketch corners: a round that would use up an arc, or where the curves meet smoothly, is refused (#8813)
* Sketch corners: a round that would use up a whole arc is refused

A quarter arc of radius 5 running into a line along it, rounded with
radius 5, put the round's tangent point at the arc's other end and left
an arc of no length. round_corner now skips a round whose tangent point
falls on either curve's other end; with none left it says that radius
does not fit, as for two lines (found by a local trim fuzzer).

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

* Sketch corners: a round where the curves meet smoothly is refused

A line running on from an arc's end along its tangent (off only by rounding)
has no corner, but the round's tangent points landed within 1e-7 mm of it and
the round came out as an arc of no length (found by a local trim fuzzer). The
corner test's line met its arc this way too and was checking that output; it
now meets the arc at a corner.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:23:17 +00:00
Nathan Dane d95f40ac88 Ribbon: pressed state for commands that stay on (#8546) (#8555)
* claim: #8546

* Ribbon: pressed state for commands that stay on (#8546)
2026-10-30 06:20:31 +00:00
austin62jake 82ac9a6b03 sketch.inspect: each profile loop names a curve once, not once per segment (#9179) (#9186)
* claim: #9179

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

* sketch.inspect: each profile loop names a curve once, not once per segment (#9179)

A profile records one curve id per piece its loop walks, and a conic or
spline is walked as many short pieces, so sketch.inspect listed a single
Bezier curve 50 times in a profile's `curves` and as often in a hole loop.
The reply now names each curve of a loop once, in the order the loop walks
them (solidcraft_sketch::distinct_curves, as delete and profile references
already do). The solved profiles keep their per-piece lists.

The lists are now a profile pick as they stand: a whole_boundaries pick
refuses a curve named twice, and the regression test extrudes the wall
from the reply's lists.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 06:14:06 +00:00
Homero Thompson 6ab46b6488 fix(#7058): the selection tools reach the geometry an SVG import leaves in the document (#7368)
An SVG import leaves a sketch with regions and no body. The marquee (box select) took only the bodies, so the imported geometry was unreachable with the selection tools: a marquee over it selected nothing, and a click on a region did not take it either.

The marquee now takes the regions of the shown sketches when the design has no body. A design that already has a body keeps the old exclusive behaviour: the marquee takes the bodies alone, not the regions of the sketches that are shown. A dialog that asks for edges or faces (the Patch dialog) never takes the regions: they are neither.
2026-10-30 06:03:16 +00:00
Matthew Knutson 991082fbaf Typed contracts for the construction axes, points and planes (#2829) (#7750)
* claim: #2829 (construct.* contracts)

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

* Typed contracts for the construction axes, points and planes

The 19 construct.* commands get input contracts and FeatureResult as
their output, with shared types for references, planes, a plane or
face base, an axis line and a construction plane's name, each calling
the reader its handler uses (split out of geo, plane_param, the face
capture and named_axis_line).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:03:04 +00:00
github-actions[bot] 4d338ba2eb Regenerate generated files and fold pending files [skip ci] 2026-10-30 06:02:42 +00:00
AsTechEvolves 94b399e98f Preference: keep the view when a sketch opens; Look At faces it on request (#9188) (#9190)
* claim: #9188

* Preference: keep the view when a sketch opens, Look At faces it on request (#9188)

Preferences > Sketch > "Turn to face a sketch when it opens" (on by default,
so nothing changes unless it is turned off). Off: creating or editing a
sketch leaves the view as it is, a sketch started on a face no longer
re-centres on the picked point, and Look At (the palette button, Ctrl+8)
still turns to face the sketch. The control channel sets it as
`ui.prefs {"sketch_open_look_at": false}`.

Changes a default: no

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 05:32:56 +00:00
Homero Thompson 5aa41703f7 Control channel: ui.home refuses a sample index that names no sample (#9087) (#9212)
* claim: #9087

* fix(#9087): ui.home refuses a sample index that names no sample

The control channel passed the sample index straight to open_sample, which drops an index that names no sample and says nothing. The request then replied ok, so an agent could not tell a typo from a sample that had opened.

The index is checked before it reaches open_sample: an index that names no sample is refused with the valid range in the error. A sample that exists still opens, as before.
2026-10-30 05:27:06 +00:00
austin62jake d9aa73b597 Measure: an exact circle's or arc's length agrees with its circumference (#9086) (#9227)
* claim: #9086 (edge lengths)

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

* Measure: an exact circle's or arc's length agrees with its circumference (#9086)

Measure gave a circle's or an arc's length_mm as the length of the
polyline drawn for it, which runs inside the curve, while the same reply
gave its circumference or arc_length from the exact circle. When the edge
is known exactly (the kernel's circle, or a sketch's own circle or arc),
length_mm is now its radius times its angle. A circle fitted to a mesh
body's polyline keeps the polyline's length.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 05:27:00 +00:00
github-actions[bot] 6e4e94a8a8 Regenerate generated files and fold pending files [skip ci] 2026-10-30 05:30:32 +00:00
github-actions[bot] ae2a5b2c0d Regenerate generated files and fold pending files [skip ci] 2026-10-30 05:24:44 +00:00
railconstr 81cbcef31c Sketch: add construction-line toolkit (#8239) (#8679)
Co-authored-by: railconstr <335242259+railconstr@users.noreply.github.com>
2026-10-30 05:02:54 +00:00
semmelrinde e3d9090e6c Russian: translate the 580 messages the catalog lacks, Render panels included (batch 7) (#9231)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:54:25 +00:00
SilentWolf75 b8e93270b4 Sketch: align view to plane when editing a rotated component sketch (#9029) (#9042)
* claim: #9029

Co-authored-by: Cursor <cursoragent@cursor.com>

* Square sketch axes when looking at a turned plane (#9029)

Look At only yaw-aligned sketches facing world up/down, so a sketch on a rotated component stayed tilted on screen. Roll (or yaw at the poles) so the sketch's x runs right and y is upright.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 04:27:24 +00:00
SilentWolf75 ef628d1c86 Canvas: pick Calibrate points on the image (#9108) (#9208)
* claim: #9108

Co-authored-by: Cursor <cursoragent@cursor.com>

* Canvas calibrate: pick Point A and B with viewport clicks (#9108).

The Calibrate card only had typed coordinates, so users could not mark the image. Clicks on the canvas now fill A then B, with on-image markers and a short hint.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Calibrate canvas: ja/zh/ar/he/vi strings for the click hints

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:25:52 +00:00
SilentWolf75 d93b1cb282 Sketch Trim: prefer free curves over projected edges (#8979) (#9224)
* claim: #8979

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sketch Trim: prefer free curves over projected edges (#8979).

When Trim/Extend/Break is active, linked face edges yield to nearby drawn curves, and the refuse message names Break Link.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 04:25:45 +00:00
Derek Van Allen a66a517c40 Persistent naming: a proposal from a second implementation (#7990)
* Persistent naming: a proposal from a second implementation

A design proposal for the persistent-naming failures still open (#4341,
#5198, #5308, #5857, #5858, #5860, #5861), from building persistent naming
in Form Studio: fall back by lineage, never by geometry alone; a suppressed
maker is not a lost reference; names saved as structured one-level values;
ordinals from the feature, not the kernel's face order; split picks resolve
to all pieces; body identity apart from the display name. Documentation
only.

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

* Persistent naming proposal: body identity follows the #5308 plan

Change 6 now agrees with bluecmd's plan on #5308: an id minted from
next_id and stored per feature slot, better than the derived identity
the first version proposed. It adds that a stored id is only as stable
as the slot assignment, which today follows each lump's least face
name, and gives the lineage signature from the #5308 discussion.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:17:54 +00:00
semmelrinde f3ee34d372 English: American spelling throughout the base catalog, British only in English (UK), and "1 body" (#9085) (#9194)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 04:16:23 +00:00
SilentWolf75 c94d1e9894 Inspect: keep analysis legends clear of left History (#4515) (#9210)
* claim: #4515

Co-authored-by: Cursor <cursoragent@cursor.com>

* Inspect: keep analysis legends clear of a left History (#4515).

Draft, Accessibility and Curvature legends were painted at the viewport's left edge under a left History sidebar. Place them in the free workspace content instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 03:59:33 +00:00
kaltenbrunnermarkus 46a2d6b53c ui-egui offscreen tests pick their GPU by SOLIDCRAFT_GPU (#9052)
offscreen_device() asked wgpu for its default adapter and ignored the
SOLIDCRAFT_GPU policy that headless pictures follow, so with
SOLIDCRAFT_GPU=software the ui-egui offscreen tests still opened the
hardware GPU. Under hardware or software the adapter now comes from
solidcraft_gpu::select (new pick_among, which pick uses too); under cpu
there is none; unset (auto) keeps wgpu's default adapter as before.

Part of #9040.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:55:09 +00:00
austin62jake c9935007e8 Sketch: the Sketch Grid box in the Sketch panel turns the sketch's grid off (#9147) (#9187)
* claim: #9147

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

* Sketch: the Sketch Grid box in the Sketch panel turns the sketch's grid off (#9147)

The box sets the open sketch's hide_grid, but the viewport drew the grid
whenever a sketch was open, so hide_grid only reached the ground grid and
the box did nothing. An open sketch now shows its grid whatever the Grid
switch says, unless its own Sketch Grid option is off; the window guides
that renders and the control channel use follow the same rule.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 03:55:05 +00:00
ABDULLAH 7b9d37c787 Arabic: Off wording in the recent snap note matches the other preference notes (#9215) 2026-10-30 03:52:57 +00:00
JK3D 8a6a064d37 Headless pictures: draw counts come back with the picture they count (#7521) (#7522)
* claim: #7521

* fix: headless pictures return their draw counts with the picture

`the_renderer_counts_what_it_draws` drew with `Capturer::rgba` and then
read `Capturer::last_stats()`. The capturer is shared with the binary's
other tests, which draw on it from parallel threads, so another picture
could land between the two calls and the test read its counts (e.g. 10224
triangles for the empty design).

`Capturer::rgba_with_stats` returns the picture and its counts, read while
the call still holds the device. `last_stats()` is removed; this test was
its only caller (the performance overlay reads the viewport renderer's own
stats handle).

New test `pictures_drawn_at_once_each_count_their_own` draws empty and box
designs from eight threads on the shared capturer. Against the old pair it
failed 25 of 40 full runs of the `capture` binary; it passes with the new
call.

* fix: a headless picture waits for its read-back's answer

A picture maps its read-back buffer, waits for its submission with
`device.poll(PollType::Wait)`, and then checked once with `try_recv`
whether the map callback had run. wgpu calls a finished mapping's
callback from whichever thread next maintains the device, in a poll or
in any `queue.submit`, after it lets go of the device. When another
thread got there first, the wait returned with the mapping done but its
callback not yet called, and the picture failed with "not supported yet:
reading a picture back without waiting for the browser", the error meant
for a browser.

On the shared capturer, the other thread is another picture's device
check (`probe_lost` submits nothing before it takes the capturer's lock),
so the `capture` tests failed now and then: `bad_picture_sizes_are_errors`
(its last 8 x 8 picture), `pictures_show_a_plain_cut_without_the_hatch`,
`pictures_blend_a_line_that_is_not_opaque`. In the app, the window's
frames are submitted on the device its thumbnails and `ui.render`
pictures are drawn on.

A native picture now waits for the answer, up to the picture timeout; a
browser still answers later, on its own.

New test `pictures_read_back_while_another_thread_uses_the_device`: 2,000
small pictures while another thread polls their device. Without the fix
it failed 19 of 20 runs (1 to 7 pictures each); with it, 150 of 150 runs
pass. `bad_picture_sizes_are_errors` now prints the error it gets.
2026-10-30 03:19:33 +00:00
github-actions[bot] f35f3197c3 Regenerate generated files and fold pending files [skip ci] 2026-10-30 04:44:58 +00:00
github-actions[bot] 78499f44af Regenerate generated files and fold pending files [skip ci] 2026-10-30 04:36:06 +00:00
semmelrinde 5b5201f1a6 Galician: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9162)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:02:11 +00:00
semmelrinde f28ff15ffb Hungarian: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9154)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:01:02 +00:00
semmelrinde 4f8bbec9fb French: translate the 356 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9153)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:00:21 +00:00
semmelrinde 77ebf1326c Italian: translate the 377 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9155)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:00:09 +00:00
github-actions[bot] fdc95337cf Regenerate generated files and fold pending files [skip ci] 2026-10-30 04:11:55 +00:00
github-actions[bot] 0e053e5e61 Regenerate generated files and fold pending files [skip ci] 2026-10-30 04:02:58 +00:00
semmelrinde 6bbb4276c0 Romanian: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9151)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:00:02 +00:00
semmelrinde 6d641c87c4 Malay: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9160)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:59:47 +00:00
semmelrinde 553f5fb9b8 Czech: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9152)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:59:46 +00:00
semmelrinde 88dd699715 Swedish: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9157)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:59:38 +00:00
semmelrinde c092e0551b Dutch: translate the 383 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9156)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:59:14 +00:00
semmelrinde c320dc421c Turkish: translate the 369 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9158)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:58:58 +00:00
semmelrinde 0f489a5777 Croatian: translate the 384 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9165)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:58:49 +00:00
semmelrinde 3df709be63 Norwegian Bokmål: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9163)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:58:45 +00:00
semmelrinde 875ac2ce49 Traditional Chinese: translate the 385 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9159)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:58:31 +00:00
semmelrinde 698275874f Indonesian: translate the 386 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9161)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:58:10 +00:00
semmelrinde 0d4470cd8c Bosnian: translate the 384 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9164)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:57:53 +00:00
semmelrinde 5dfc3481c1 Serbian: translate the 384 messages the catalog lacks, Render Light and Frame stages included (batch 7) (#9166)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 02:57:11 +00:00
bherbruck 8f1b456275 Release 0.27.1 (#9181)
* Release 0.27.1

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

* Render job test: wait for the cancelled job's reservation to be released

The worker drops a job's reservation just after the job turns terminal, so
checking admitted bytes right after wait_timeout raced (10 of 40 local runs
failed). Poll with a deadline, as dielectric_tests does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:34:23 -04:00
bherbruck b596bfc730 Bottom bar: keep active toggles readable when the selected fill is as dark as the accent (#9168) (#9171)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:42:27 -04:00
Colin Faulkingham d11d268597 Render: reject excessive pixel work during preflight (#5088) (#5728)
* claim: #5088

* fix: reject excessive render pixel work during preflight (#5088)
2026-10-30 01:35:12 +00:00
github-actions[bot] 4b0ba62c69 Regenerate generated files and fold pending files [skip ci] 2026-10-30 01:13:55 +00:00
taylorhayis 313d2bdb41 Sketch: snap where two recent centre and midpoint guides meet (#8694) (#8744)
* Sketch: snap where two recent centre and midpoint guides meet (#8694)

The alignment guide remembered one reference and did not move the cursor. Keep the previous centre or midpoint as well, and snap a drawing point to where their guides cross.

* Sketch: keep the recent-guide crossing snap off until asked (#8694)

The crossing snap was on whenever alignment guides were. Give it its own Sketch preference, off by default, so the guides stay visual until someone turns the snap on.

* Sketch recent snap: ar/he/vi strings and clippy borrow fixes

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-30 00:40:05 +00:00
Colin Faulkingham 079f6e61e6 Sketch: show no geometry for an empty sketch (#9077) (#9104)
* claim: #9077

* Sketch: show no geometry for empty viewport sketches (#9077)
2026-10-30 00:33:47 +00:00
Rick Morgans 4a781b8cd8 MCP: --param-check checks calls to the running app (#9072) (#9073)
* claim: #9072

* MCP: --param-check checks calls to the running app (#9072)

`mcp --connect` (and `mcp` once it bridges to an app) took
`--param-check` and forwarded every call unchecked, while the app's own
session is always off. `Remote` now checks each `engine.execute` and
`engine.batch` against this server's command contracts before sending:
`strict` refuses a call, or a batch with such a step, without sending
anything, and `warn` adds `param_warnings` to the app's reply (each
step's result in a batch). A command with no contract here goes to the
app unchecked. `Auto` carries the headless session's level over when it
bridges to a late app. `batch_preflight::contracts` is the contract half
of the existing preflight, for a runner in another process.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:33:42 +00:00
semmelrinde 87206f20c8 Theme: the System theme leaves the window to follow the system appearance (#8983) (#9103)
theme::apply always sets egui's theme to light or dark, and egui's own window
sync then told the native window to be light or dark. A macOS window told so
keeps that appearance and no longer reports the system appearance changing,
so with Theme set to System the app stayed light after macOS turned dark. The
app now syncs the window itself: the system's appearance while the theme
follows it, the theme's own otherwise (for the title bar).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:51:07 +00:00
Logan Night 5a24d4095a Sketch: an old edge-on projection keeps its line when it is re-resolved (#9025) (#9097)
* claim: #9025

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

* Sketch: an old edge-on projection keeps its line when it is re-resolved (#9025)

A face seen edge-on was saved as two lines on the same two points, one each way, and is one line now. Re-resolving the link dropped both lines and built a new one under a new id, so an offset that names the first line as its source failed with "an offset source vanished or changed shape". Keep the first line and drop only the second.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-29 23:50:29 +00:00
bherbruck 3e62553d9c Release 0.27.0 (#9106)
Fold the pending design format changes (sketch-equation-curves, timeline-joint-folders) into format 44.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:52:57 -04:00
semmelrinde d4fb5538e6 Sketch: a side squeezed to a hair in two solves is refused too (#9049)
The check for a constraint that shrinks a curve to a point took under 1e-6
for a point. One solve could squeeze a 49 mm side to about 1.2e-6 mm and
the next take it the rest of the way, so neither solve tripped it (two
rectangles' corners made coincident, found by a local sketch fuzzer). Under
1e-5 now counts: in 4000 random sketches the constraints that still
collapsed a line went from 9 to 0, and every refusal rate stayed the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:52:36 +00:00
github-actions[bot] f6393ef211 Regenerate generated files and fold pending files [skip ci] 2026-10-29 22:12:44 +00:00
SilentWolf75 355b486595 Parameters: unit-less numbers follow the other side's unit (#9015) (#9027)
* claim: #9015

Co-authored-by: Cursor <cursoragent@cursor.com>

* Evaluate bare length offsets in the design's units (#9015)

Feature expressions like baseLength - 1 were subtracting millimetres because val() parsed with mm defaults and only applied design units to a final unit-less result.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 21:52:23 +00:00
Rick Morgans 2356a63aea CLI: run takes --param-check (#6921) (#6924)
* claim: #6921

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

* CLI: run refuses unknown options and takes --param-check (#6921)

`solidcraft-cli run` now checks its options through `known_args`, so a
mistyped option fails instead of being ignored, and takes
`--param-check off|warn|strict` with the MCP server's words and default
`off`. `warn` prints each unknown key with a suggestion on stderr and
runs the step; `strict` stops at that step and writes nothing.

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

* CLI: run's option check takes --param-check; warn on timed runs as steps complete

`run` keeps #6919's (#6918) unknown-option check, with `--param-check`
among the options that take a value. `Session::run_script_timed` now
takes the same per-step callback as `run_script_each`, so `run --timings`
or `--profile` at the warn level reports a step's warnings even when a
later step fails, instead of after the script (and not at all on a
failure).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:40:30 +00:00
Steven Waters 57c0121cb3 Cut & paste features between components (#5798) (#5805)
* Add timeline.cut and refactor timeline.paste to allow cutting and pasting features

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

* Updated test for new grounded behaviour

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

* Resolved conflicts with #3390

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

* Fixed failing test

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

* Resolved conflicts with #3512

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 21:39:53 +00:00
austin62jake 9b97e06f71 Web: open tabs share settings changes, so one tab no longer undoes another's (#8644) (#8832)
* claim: #8644

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

* Web: open tabs share settings changes, so one tab no longer undoes another's (#8644)

Every open tab of the web app saved its whole copy of the settings to the
same browser storage, every 30 s and on focus loss, so a tab still holding
old settings put them back over a toolbar change made in another tab.

A tab now writes its settings in the frame they change, and the other open
tabs load them from the browser's storage event, so their own saves keep
them. Settings loaded that way aren't written back.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 21:36:10 +00:00
Colin Faulkingham 9c5a636987 Parameters: accept mixed-case text units (#8960) (#8985)
* claim: #8960

* fix: accept mixed-case text parameter units (#8960)
2026-10-29 21:11:16 +00:00
SilentWolf75 18b00a4192 UI: keep Preferences above the Distance entry (#9016) (#9022)
* claim: #9016

Co-authored-by: Cursor <cursoragent@cursor.com>

* UI: keep Distance entry under Preferences and Report a bug (#9016)

Canvas value boxes used Foreground order, so they covered system windows. Draw them as Background chrome instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 21:09:28 +00:00
ABDULLAH 988caa83fc Arabic: settings profiles, ribbon reveal, orbit, viewport and history marker wording (#9019)
* Arabic: Settings profiles and command search ribbon reveal wording

* Arabic: orbit, viewport and history marker terms in today's new strings
2026-10-29 21:03:28 +00:00
semmelrinde 18dc05c83d Sketch Trim and Extend: a crossing at a curve's own end is that end, leaving no curve of no length (#9012)
* Sketch Trim: a cut at a curve's own end leaves no piece of no length

A curve that ends where another one ends crosses it there only within the
intersection's tolerance (1e-6), but bracket dropped cuts at the curve's
ends only within 1e-9 of its parameter range. A line ending on an arc's end,
trimmed from its other end, was cut a hair inside its own end and kept a
piece about 1e-7 mm long (found by a local trim fuzzer: 78 such trims in
4000 sketches, and 8 breaks). bracket now also drops a cut within that
tolerance of the curve's ends, as add_cut merges cuts that close together.

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

* Sketch Extend: a touch at an arc's growing end is that end, not a stop

An arc whose end is where a line ends that touches the arc's circle there
found the touch a hair beyond the end (within the intersection's tolerance),
stopped on it, and added an arc from that point to itself. A hit within that
tolerance of the growing end (1e-6, as for the far end) is now the end.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:48:18 +00:00
semmelrinde 5cf18ba8c3 Sketch: a constraint that would shrink a curve to a point is refused (#8988)
A constraint whose only solution squeezes curves to a point (a rectangle's
corner made the midpoint of its opposite side, or two opposite corners made
coincident) collapsed the whole rectangle while the sketch said solved. A
solve that shrinks a line or circle from over 1e-3 to under 1e-6 now counts
as one that fails, as one that throws points far away does: a strict edit is
refused, another keeps the sketch from before its solve, and the free solve
after the held ones in apply_in_order refuses the constraint.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:21:51 +00:00
P'tite Tête 9454afe13a fix: Draw only the timeline chips in view (#8976)
- Skip drawing and interaction for history chips outside the strip's visible part, after their slot is recorded
- Keep a dragged or focused chip, and every chip while a popup is open
- Frame on a 4,880-body design: 20.3 ms -> 14.6 ms (M4 Max, release, headless)
- Test a_long_timeline_paints_only_the_chips_in_view

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-29 19:29:54 +00:00
MATHEUS PINHEIRO RAPOSO 70d39bb4f3 Three tests pass on Windows with an 8.3 TEMP path (#8968)
On a Windows account whose %TEMP% is a short (8.3) path, such as
C:\Users\MATHEU~1\AppData\Local\Temp, three tests failed because they built
their folders from std::env::temp_dir(), and the agent file scope refuses short
names ("not allowed for this agent: short (8.3) names such as `PROGRA~1`"):

- engine: cmd::render::settings_tests::embedded_hdr_capture_rejects_parameters_scope_and_cpu_primaries_without_mutation
- ui-egui: agent_chat::tests::file_actions_wait_for_the_user
- ui-egui: agent_chat::tests::permission_modes_decide_what_asks

They now use vfs::temp_dir(), which spells the folder with long names, as
libraries_tests and motion_tests already do (#1385, #1211).

Checked on Windows 11 x64 with the 8.3 TEMP: the agent_chat and
render::settings tests (113) pass, and clippy -D warnings is clean for both
crates.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:13:36 +00:00
github-actions[bot] a4fd998539 Regenerate generated files and fold pending files [skip ci] 2026-10-29 19:40:05 +00:00
github-actions[bot] 1c673e38c7 Regenerate generated files and fold pending files [skip ci] 2026-10-29 19:31:29 +00:00
tr1pp4 b43712cdb0 Command search: show a command's place in the ribbon, also inside section menus (#7287) (#7322)
* claim: #7287

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

* Command search: show a command's place in the ribbon, also inside section menus (#7287)

Hovering a command result in Search commands switches the ribbon to the command's
tab and marks its button with its tooltip; a command that has no button there
opens its section's drop-down on its row instead. Leaving the results (or closing
the box) goes back to the tab the user was on, so pressing S, typing and Enter
works as before.

Starting a command from the box makes its button or section label blink twice,
and a command started from a section drop-down marks that section's label while
it runs, like a pressed button.

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

* Command search: the ribbon reveal is a preference, off by default (#7287)

Preferences › Interface › Show search results in the ribbon (`ribbon_reveal`) turns on the hover
preview, the blink after a start and the running mark on a drop-down's panel label. Off, the
ribbon behaves as on main.

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

* Pending locale: Arabic, Hebrew, Japanese, Vietnamese and Chinese for the ribbon reveal setting

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-29 19:06:44 +00:00
Homero Thompson 184d4720be Sketch: a dimension can measure a model edge (#7912) (#8010)
* claim: #7912

* Sketch: dimension a sketch entity against a model edge

A dimension took sketch entities only, so measuring a sheet-metal body's edge
was refused with "cannot dimension that combination of entities". The
entities now take a model reference too, an {edge: [x,y,z], body?} or a
{vertex: [x,y,z], body?}, which is projected into the sketch first, as the
constraint commands' operands are since #6348, and measured there.

The Dimension tool picks a body's edge or vertex when the sketch has nothing
under the cursor, and reads the pick back into the reference the command
takes.

* style: rustfmt the sketch dimension command

* Cap the entities one call may project

A dimension projects the model references among its entities into the
sketch, one link and one resolve each, and the loop had no cap: a single
request could make the engine resolve thousands of model edges before
the dimension refused the list. The count is now refused before anything
is projected, and the projection loop stops at the same limit the
Project command uses.

* Sketch dimension on a model edge: use the sketch store after the store refactor

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-29 18:54:42 +00:00
Colin Faulkingham f08623be3c Fix V visibility shortcut for browser folders (#8800) (#8925)
* claim: #8800

* Fix folder visibility shortcut in browser (#8800)

* retrigger CI [skip ci]

* retrigger CI
2026-10-29 18:54:34 +00:00
semmelrinde 31218b1451 Sketch drag: an end the cursor can't reach stays where it is (#8944) (#8967)
The cursor correction tries the constraints' free directions a whole step at
a time and took a step as soon as the cursor's distance fell, by any amount.
Along freedom that can't reach the cursor (the far end of a line held only
by its direction), retracting onto the constraints still moves the cursor by
rounding, so such steps were taken: the end walked away a step at a time, and
a line measured by an angle could shrink to nothing, as in #7635. A step now
has to move the cursor itself by more than the solve's tolerance.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:54:27 +00:00
tr1pp4 cb34eef6aa Settings profiles: save, switch, export and import input and interface settings (#6237) (#6254)
* claim: #6237

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

* Settings profiles: save, switch, export and import input and interface settings (#6237)

A profile keeps the Interface, Navigation and Sketch preferences (mouse mode,
radial menu and gestures with every studio's lists, theme, scale, font, ...),
the language, the keyboard shortcuts, the toolbar, the S box favourites and
the imported theme it uses under a name. Machine-specific settings stay out
(3D mouse, graphics, units, AI, recent files).

- Preferences > General > Settings profiles: choose the active profile to
  apply it, Save, Rename, Delete, Export... and Import...; a note shows when
  the settings differ from the saved profile; Cancel takes a switch back.
- settings.profile.{list,save,apply,rename,delete,export,import} commands for
  the control channel, palette and scripts; the engine checks the parameters,
  the app does the rest.
- One `solidcraft-profile/1` file; import checks the format and name, drops
  unknown keys and unreadable values, caps sizes, never switches, and never
  panics. Each preference is applied through prefs::set, so a value this
  computer refuses keeps its current one and is reported.

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

* Glossary: settings profile, and "profile" among the words with more than one meaning (#6237)

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

* Settings profiles: new strings in a pending locale file, not the catalogs (#6237)

New UI strings now go to assets/locales/pending/ (CONTRIBUTING.md), so
this pull request no longer edits the shared catalogs.

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

* Preferences snapshot: compare the toolbar, favourites and profile too

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

* Settings profiles: keep a command's several keys (#6237)

Since a command can have several keys (#6288), the keymap saves a list where it has more than
one. A profile's checks only took a single key, so saving or importing a profile dropped such a
command's shortcuts. They now read the keys as the keymap does.

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

* Settings profiles: Arabic, Hebrew and Vietnamese strings (complete catalogs) (#6237)

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

---------

Co-authored-by: Tripper <tripper@localhost>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-29 18:42:36 +00:00
github-actions[bot] b9e9521128 Regenerate generated files and fold pending files [skip ci] 2026-10-29 19:03:47 +00:00
github-actions[bot] e59a2bcf80 Regenerate generated files and fold pending files [skip ci] 2026-10-29 18:56:56 +00:00
Logan Night 6fcdca1bb7 Dialogs: a pre-selected sketch line lying on a body edge goes in as that edge (#8927) (#8942)
* claim: #8927

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

* Dialogs: a pre-selected sketch line lying on a body edge goes in as that edge (#8927)

With a finished sketch shown, a click on a body edge that one of its lines
lies on picks the sketch line. Fillet and Chamfer take edges and no sketch
curves, so the pre-selection found no input to fill: the tool opened with
Edges empty and the selection was dropped, and only a second click with the
tool open (which picks the edge) made it work.

When nothing in the dialog takes a pre-selected sketch curve but an input
takes edges, look for the body edge the curve lies on from end to end
(display polylines, with room for a curved edge's chord sag) and take that
edge instead.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:26:56 +00:00
CamWight a985b8ee17 Preferences: Perspective view's field of view, a slider or a typed value (#5364) (#5373)
Preferences > Graphics > Viewport gets a Field of view row under Perspective view: the UI kit's
slider in degrees, 15 to 90, dragged or typed and kept to whole degrees, enabled only while
Perspective view is on. The preference is perspective_fov, in radians (default 0.6, about 34°,
so the default view draws exactly as before), and Restore Defaults puts it back.

One place decides the angle the view is drawn with: SolveApp::view_fov(), the preference with
Perspective view on and 0 with it off or while a sketch is open. A saved value outside 15°-90°
reads as the default. projection and fit_bounds read it (fit_bounds' second hard-coded copy of
the 0.6, tan(0.3), now goes through Camera::half_height), and so does the headless fit.
perspective_view() stays, as view_fov() > 0, so #4291 needs no change.

When the angle changes from one perspective angle to another, the camera's distance scales by
tan(old/2) / tan(new/2), with a running view animation's ends, so the target keeps its size and
the slider changes the depth, not the zoom. prefs::follow_view_fov does it once per frame and
after ui.prefs, Cancel and Restore Defaults; a parallel view keeps the last perspective angle,
and while Render is open the camera is Render's own, so the change waits for the design view.

The Render workspace's Perspective check box turns perspective on at the preference's angle,
not π/4; a saved render setup keeps its own. ui.prefs takes perspective_fov and refuses values
outside the range; ui.camera reports the angle the window draws with, and checks a replayed
camera against it, so its reply still replays. Field of view is in en, ar, de, he, ja, ko, vi and zh;
the ui.prefs settings list in docs/control-protocol.md and the guide's tour describe it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:23:49 +00:00
HiroKenEz c857f2fe70 parameters.add: a parameter with no unit takes the design's units (#8865) (#8880)
* claim: #8865

* parameters.add: a parameter with no unit takes the design's units (#8865)

A new parameter given no unit took "mm" for any length or bare number, so
in an inch design `parameters.add {name: v, expression: "40/2"}` made a
20 mm parameter where the Parameters dialog makes 20 in. The unit a new
parameter infers is now a length in the design's units (mm designs are
unchanged); parameters.change on a new name and parameters.import share
the same inference. Angles and unit-less values keep their units.

A definition typed into a value of no fixed kind (a coupling's pitch,
a joint's value) still reads its bare number as millimetres, as that
field does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:18:04 +00:00
SilentWolf75 9416e975e0 View cube: rapid clicks add full turns from the in-flight target (#8912) (#8929)
* claim: #8912

Co-authored-by: Cursor <cursoragent@cursor.com>

* View cube: rapid clicks add full turns from the in-flight target.

Match arrow-key retargeting so a second cube click turns from the glide destination instead of the mid-animation camera (#8912).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 18:17:02 +00:00
kaltenbrunnermarkus 5f11d92458 Opening a design pins its body names, so the first undo evaluates nothing (#8939)
Problem: a design saved before body names were pinned (#6403) is pinned by its first command
that changes it, as part of that command. Undoing that command went back to the unpinned design,
whose fingerprints no evaluated result matched any more, so the first undo after opening
evaluated the whole timeline again: 63-72 s in the SELENE rover (5,484 features) when opened
from the command line or headless (`Session::opened_at`), 0.08 s with this change. `doc.open`
pinned at the end of the command instead, which kept undo fast but left the design with unsaved
changes on opening.

Solution: `Session::opened_at` pins the names as it opens the design, with the names the file's
bodies have (no result changes, so nothing is evaluated), and takes the pinned design as the
saved one: opening is no change.

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

Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com>
2026-10-29 18:15:47 +00:00
SilentWolf75 97550dc37e Parameters: one Name-column floor for every table in the dialog. (#8945)
Favorites, User and Model sections stay lined up when only some lists hold a long name (follow-up to #8551).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 18:14:35 +00:00
ABDULLAH 4456e0470a Arabic: Preferences instead of Settings, hover and Project option wording (#8949)
home.settings and ai.connected_toast follow the English change from Settings to Preferences (التفضيلات, as ui.preferences). Hover is no longer rendered as scrolling in the browser and navigation bar options, the Project options dialog label is fully in Arabic, and the trackpad zoom key help says to hold the key.
2026-10-29 18:14:26 +00:00
HiroKenEz 561336aab6 Text fields: a right-click menu with Cut, Copy, Paste and Select All (#8875) (#8882)
* claim: #8875

* Text fields: a right-click menu with Cut, Copy, Paste and Select All (#8875)

Every kit text field (dialog and sketch value fields, the sketch dimension
box, canvas value boxes, names, text areas, passwords, read-only text) now
calls one shared ui_kit::text_menu::menu after drawing its editor. Rows are
disabled when nothing is selected, the text is read-only or a password, or
the clipboard holds no text. The edit marks the response changed and gives
the focus back, so owners that commit on lost focus keep the field; a right
press inside the selection keeps it; Esc closes only the menu; expression
suggestions wait while it is open. New message ui.select_all in a pending
locale file; Cut, Copy and Paste reuse ui.cut, ui.copy and ui.paste.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:14:09 +00:00
Daniel 82b38f5bae Timeline: delete after the marker counts and deletes the shown features (#8860) (#8866)
* claim: #8860

* Timeline: delete after the marker counts and deletes the shown features (#8860)

With the bottom timeline filtered to one component, "Delete N Features After
History Marker" took the marker as a position among the shown features but
sliced the design's feature list with it. N counted features of other
components, and confirming deleted them too, including features before the
marker.

The entry now gets the feature's design position, the design's marker and
the shown positions, and deletes the shown features after the marker; the
label counts the same set. The history beside the browser shows every
feature and passes no filter, so it behaves as before.

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

* Timeline: look up shown positions by binary search, test the entry before the marker (#8860)

The shown positions are ascending, so a binary search finds each one without
walking the list for every feature after the marker. The test now also checks
that a shown feature before the marker does not offer the entry.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:03:31 +00:00
github-actions[bot] 43a22bed46 Regenerate generated files and fold pending files [skip ci] 2026-10-29 18:25:46 +00:00
bherbruck cd12d9d8e7 Scenario: set the history sketches layout through ui.prefs (#8961)
A test-only change: the scenario no longer clicks in the growing Interface page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:23:29 -04:00
github-actions[bot] 15e3245180 Regenerate generated files and fold pending files [skip ci] 2026-10-29 18:19:08 +00:00
semmelrinde 730dc5f6c3 Segmented choices: show the options in the user's language (#8030)
A segmented control measured its options translated but painted them as
the caller passed them, so the many callers that pass English labels
(the Coil's Circular | Square and Inside | Center | Outside, Revolve's
One Side | Two Sides | Symmetric, Shell's direction, ...) showed English
in every language. Only where the row was too narrow and the control
fell back to its dropdown, which translates, did they read right.
Segments now paint, size, describe and tip their options translated;
the handles keep the option as passed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:55:25 +00:00
Colin Faulkingham 3cdc68aaf1 Viewport: opt-in model zoom and Traced 3D mouse orbit (#7435) (#7991)
* claim: #7435

* Viewport: the 3D mouse, the bridge and drag-zoom stop at the model, like the wheel (#7435)

The wheel and pinch hold the model point under the cursor, so the view
comes up to the surface and stops there. Every other zoom path passed no
anchor, so the 3D mouse cap, the navigation bridge and drag-zoom zoomed
straight through the surface into the body.

A device has no pointer, so the cap and the bridge now hold the model
point in the middle of the view; drag-zoom holds the point under the
pointer, like the wheel. With "Zoom to the cursor" off the old
zoom-through-the-model behaviour stays, as it does for the wheel.

The orbit half of the issue is a design decision and is untouched.

* Viewport: drag-zoom holds the point where the drag started (#7435)

The zoom anchor followed the moving pointer, so the zoom slid away from
what was pressed on as the drag went on. It now holds the press point,
the way the wheel holds the point under the cursor.

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

* 3D mouse: turn around the model in the middle of the view (#7435)

The cap and the navigation bridge turned the view around the view's
centre, which after a pan or from far away sits behind the face, so the
part swung away while it turned. They now turn around the model point in
the middle of the view (the middle of the bodies in view over empty
space), and keep it still on screen. Preferences > Navigation > "Turn
around the model in the middle of the view" switches back to the old
behaviour.

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

* 3D mouse: keep the turning point until the cap is back at rest (#7435)

The point the cap turns around was picked again every frame, so it slid
to whatever came into the middle as the view moved. It's now chosen when
the cap leaves rest and kept until the cap comes back, the way a mouse
orbit keeps the point it started on.

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

* 3D mouse: show the point the view turns around (#7435)

While the cap turns the view, a faint ball in the accent colour marks the
model point it turns around, so it's clear what the turn holds still. The
point is now picked when the cap starts turning, not when it only pans or
zooms, so a pan shows no ball.

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

* 3D mouse: orbit is a choice of Traced or Classic (#7435)

The checkbox becomes a two-way choice on the 3D mouse section's form:
Traced turns around the model in the middle of the view when the turn
begins and marks it with a faint ball; Classic turns around the view's
centre as before. Traced stays the default.

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

* Docs: the 3D mouse's Traced and Classic orbit (#7435)

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

* Make model zoom and traced orbit opt-in (#7435)

---------

Co-authored-by: nomadsgalaxy <16734355+nomadsgalaxy@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:49:15 +00:00
ReDead e3c08471b6 Sketch: diameter box appears for a second concentric circle (#8899) (#8904)
* claim: #8899

Co-authored-by: ReDead <ReDead@users.noreply.github.com>

* Sketch: keep Diameter box when placing a second concentric circle (#8899)

A centre-point circle's press starts drag-to-draw; releasing on the same
centre (or close to the press) must keep the stage-1 Diameter field instead
of resetting it and risking a second on-click that finishes the shape.

Co-authored-by: ReDead <ReDead@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: ReDead <ReDead@users.noreply.github.com>
2026-10-29 17:48:53 +00:00
Ahmad Khan a7048f41ab Sketch: equation-driven curve, a curve from x(t) and y(t) formulas (#8352) (#8353)
* claim: #8352

* Sketch: equation-driven curve, a fit point spline from x(t) and y(t) formulas (#8352)

sketch.equation_curve draws a curve from formulas in the expression
language and the design's parameters: x(t) and y(t) over a range of t, or
y = f(x) over a range of x. The curve is a cubic fit spline through fixed
points sampled from the formulas, as many as keep it within a tolerance
(0.01 mm by default, at most 500 points); a curve that ends where it starts
closes smoothly. The sketch keeps the formulas (one new design-format
file), places the points again before every solve so they follow the
parameters, and sketch.equation_curve.edit changes them, keeping the joined
end points. Both commands have input and output schemas.

* Sketch: Equation Curve window, its Edit menu item, labels and guide (#8352)

The ribbon's Equation Curve opens a window for the form (parametric or
explicit), the formulas, the range and the tolerance; OK runs
sketch.equation_curve. Right-clicking an equation curve offers Edit
Equation Curve, which reopens the window on its formulas and runs
sketch.equation_curve.edit. New labels and help in a pending locale file
(with ar, de, he, ja, ko, vi and zh), the icon, the glossary term and the
sketching guide.

* Equation curve: one if for closing the sampled points (clippy collapsible_if)
2026-10-29 17:42:59 +00:00
github-actions[bot] 1c0001af8e Regenerate generated files and fold pending files [skip ci] 2026-10-29 17:50:55 +00:00
semmelrinde 8b1ee94296 Sketch: an edit whose solve throws points far away is not taken (#8931)
A held solve applying a constraint, or the edit's own solve, could find a far
solution of the constraints and move points millions of millimetres while
the sketch said solved. Such a result now counts as a failed one: the next
held solve is tried, or the edit is refused.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:24:02 +00:00
Ryan 791abbd9b1 Sketch: hold horizontal/vertical and line up with other points at once while drawing (#7828) (#8474)
* claim: #7828

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

* Sketch: hold horizontal/vertical and line up with other points at once while drawing (#7828)

While drawing, the pointer lines up horizontally with the nearest sketch
point level with it and vertically with the nearest one plumb with it, at
the same time, as a dragged point already does; holding H/V with the
previous point still lines up across with another point. Dotted guides run
from the points lined up with (H, V or H+V), and the click adds a
Horizontal or Vertical constraint with each, in the shape's undo step,
unless the sketch already has it.

A click takes the tool out of the app while it runs, so refine_snap
treated every tool as the Line tool then: a rectangle's click could take a
line-extension inference its hover never showed. The tool's points and
kind are kept from the last hover, as the previous point was.

Preferences > Sketch > "Line up with other sketch points while drawing"
(sketch_align_points) turns it off for the old behaviour.

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

* Sketch: lining up with other points is off unless turned on

Review on #8474: sketch_align_points defaults to off, so nothing changes for anyone who
doesn't turn it on. With it off, a click also treats the tool as before (the Line tool's
inferences), instead of keeping the tool's kind from the hover.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:11:42 +00:00
Nathan Dane 6255a10e80 Viewport status item: engine list of what is on (#8545) (#8554)
* claim: #8545

* Declare ordered viewport status items (#8545)

* Keep legacy surface analysis coexistence behind a preference (#8545)
2026-10-29 17:11:34 +00:00
Ryan d05a114241 Insert Design: pick one of the other open designs, or browse for a file (#8570) (#8571)
* claim: #8570

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

* Insert Design: pick one of the other open designs, or browse for a file (#8570)

With other designs open in tabs, Insert Design first lists them under the
button: one click inserts a saved one as a linked component; one with
unsaved changes inserts its saved file and the status line says so; one
never saved is greyed out, as there is no file to link. Browse... opens
the file picker, as the button did before, and it still goes straight
there with no other design open.

Preferences > Design > Linked designs > "Insert Design lists the designs
open in other tabs first" (insert_lists_open_designs) turns the list off.

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

* Insert Design list: sit with the other menus' items, so run_item keeps its doc comment

Review on #8571: insert_design_items had landed between run_item and its comment. The
rebased menu-walk list keeps main's Target::HistoryControls beside Target::InsertDesign.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:11:25 +00:00
PaddleStroke 6ebfe91661 History: say why an edited feature behind the marker changes nothing (#8100) (#8727)
* claim: #8100

* History: say why an edited feature behind the marker changes nothing (#8100)

Editing a feature that sits after the history marker (double-clicking its
greyed-out item) rolls back to it, previews the change, and on OK puts the
marker back where it was: the feature stays rolled back, so nothing in the
view changes and nothing says why. That is the silent "pattern of a cut
does nothing" in #8100 (the reporter's design has its pattern behind the
marker; on main the pattern itself builds fine once rolled forward).

- timeline.redefine replies `rolled_back` and, when true, a `note`; it also
  carries the feature's `warning` (for a rolled-back feature, the one its
  constructor met building it at its place).
- The window shows that note in the status line after such an edit.
- A rolled-back item's tooltip in both History layouts says it is after
  the history marker and not built.

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

* Pending locale: Arabic, Hebrew, Japanese, Vietnamese and Chinese for the rolled-back notes

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-29 17:11:10 +00:00
Logan Night d0b357d532 Sketch profiles: close the joints near-coincident curves leave open (#8639) (#8922)
* claim: #8639

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

* Sketch profiles: close the joints near-coincident curves leave open (#8639)

Two free-form curves that lie on each other along a stretch (a DXF that
carries an outline twice, with the same straight sides and different
corners) are cut into sampled pieces whose ends the graph joins into one
node within 1e-4 mm, but the pieces keep their own ends, a few millionths
apart. Only analytic edges were closed at those joints, so the sliver
loops between the two curves stayed open by up to 1e-5 mm, more than the
1e-6 a solid needs. The region around them took those loops as holes:
extruding it failed with "profile loop is not closed" and it drew no
fill when picked, so it looked unselectable.

Close a line's joint in each finished loop: line to line at their middle,
a line onto any other segment's end. On the reporter's DXF the band
between the outer two rings now extrudes (110.5 mm2 by 2 mm = 220.9 mm3),
and the zero-area slivers no longer form profiles (221 -> 216).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:11:01 +00:00
HiroKenEz cc047a29c1 Sketch: the status line after a dimension is in the interface language (#8863) (#8879)
* claim: #8863

* Sketch: the status line after a dimension is in the interface language (#8863)

The Dimension tool built its "Dimension applied" status sentences (new
parameter, renamed parameter, plain value) and the driven-dimension note
with English format! strings, so they stayed English under a translated UI.
They now come from the catalog (one pending locale file: en, ar, de, he,
ja, ko, pl, vi, zh), with the typed parameter name passed in as is.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:07:31 +00:00
Daniel 32c8b23199 History: Delete on a selected feature deletes the selection in both timelines (#8861) (#8867)
* claim: #8861

* History: Delete on a selected feature deletes the selection in both timelines (#8861)

With several features selected, Delete in a selected feature's menu deleted
the whole selection in the bottom timeline and only that feature in the
history beside the browser, which offered the selection as a separate
Delete Selected Features… entry.

Both menus now use one Delete entry: the selected features when the
right-clicked one is among them, labelled "Delete N Selected Features",
else that feature alone. The history beside the browser no longer adds its
own Delete Selected Features… to a selected feature's menu, since the entry
above covers it; folder menus keep it. The single-feature delete helper had
no callers left and is removed.

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

* History: a feature's menu leaves out Delete Selected Features… by a flag (#8861)

A right-click on a feature outside the selection selects that feature
first, so in a feature's menu the clicked feature is always selected. A
flag that says the menu is a feature's says the same without the lookup.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:06:25 +00:00
github-actions[bot] 21366ec47e Regenerate generated files and fold pending files [skip ci] 2026-10-29 17:18:00 +00:00
N3MI-DG 699d730151 Button head cap screws, ISO 7380-1, in the standard parts library (#8006) (#8017)
* claim: #8006

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

* Parts library: button head cap screws, ISO 7380-1 (#8006)

Insert Part offered the socket head cap screw but no button head, so a
design using button heads had to stand in a cap screw and accept the
wrong head. A button_head_cap_screw family now sits beside it, taking
the same parameters from the ISO 7380-1 table over M3 to M16.

The head is a new Head::Dome: the shank and the head revolve as one
outline, with the rim at dk, a short cylindrical flank, the arc of the
crown, and a flat top around the socket, so the hex socket stays a plain
cut on a plane. The standard draws the flank and the flat but dimensions
neither; they are 0.15 k and 0.7 s, and the comment on those constants
records what that costs against the standard's crown radius r1 and why
neither other fractions nor r1 taken as given does better. dk, k, s and
t are exact, each checked against three reproductions of the table.

Insert Part, Change Part Size and the bill of materials need no change:
they read the families and their table rows from parts.library.

The wider fastener request is issue 7692; this is only the one family
from it, and leaves the rest of that list open.

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

* Parts: format after the rebase

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

* Pending locale: the Button head cap screw family name

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-29 16:43:36 +00:00
Lars Jansen a4274f9b59 Line tool: press and drag draws one line (#7247) (#7252)
* claim: #7247

* feat(sketch): press and drag in the Line tool draws one line (#7247)
2026-10-29 16:34:30 +00:00
N3MI-DG d6d6c7d9ee Copy and paste bodies and components between designs (#3511) (#3512)
* Components: copy and paste bodies and components between designs and components (#3390)

Squashed for the rebase onto main.

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

* claim: #3511

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

* Paste New: copy from a design other than the one pasted into

`component_clone::paste` read the design it copies from through the session it pastes into.
`build` now takes the source (document, evaluated model, unapplied moves) apart from the
target session; Paste New within one design passes the session as both, as before. Names are
numbered against the target's names.

From another design (`Source::copied`, a `ForeignCopy` snapshot taken at copy time):
- names are kept where the target doesn't have them, else numbered;
- the user parameters the copy uses come along, `w_2` where the target has a `w`, with the
  copied expressions following;
- a copy whose features use what isn't in it (bodies, sketches, faces, parameters outside it)
  is refused with a list of what; `FeatureKind::input_bodies` lists the bodies a feature uses;
- configurations stay with their design, and the target's timeline marker must be at the end;
- a body moved in from outside comes as an imported base named `BodyImport1`.

Body transfers are pointed at the copy's components before they are added: `add_feature`
checks that the components they name exist, which another design's don't.

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

* Engine: paste bodies and components copied in another design

`component.paste_from_design` pastes what `Session::foreign_copy` holds, at the same world
coordinates as in its design, into the active component or `component` (placed by
`component_path`; a component placed more than once gets it in every instance), as one step
to undo:
- bodies as imported bodies, without their history, in one Import feature (named as any
  import is, "Import1"); a body seen in another placement ("Cube (B:2)") pastes as "Cube";
  names the target has are numbered;
- components as independent copies (Paste New from the snapshot): with their history,
  appearances, materials, joints and motion inside them; joints to the outside stay behind.

Tests: bodies (position, naming, target component, undo; a body of another placement) and
components (history, appearances, a part placed twice, editing independently, save and reopen,
undo; user parameters renamed; refused when using a body outside it; bodies moved in).

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

* Clipboard: copy in one design, paste in another

Copying or cutting bodies or components also keeps a snapshot of them (`ForeignCopy`); another
tab pastes it with `component.paste_from_design`: bodies as imported bodies, components as
independent copies (Paste and Paste New alike), folded and selected. Cut pastes as a copy
there, the original staying; Esc on the cut leaves nothing to paste anywhere.

The last copy in any design is what pastes: each copy or cut is numbered, and a tab whose own
copy is older empties its clipboard when it is switched to (`drop_stale`). A failed copy
anywhere leaves nothing to paste in any tab.

Tests: bodies and components pasted in another tab, the last copy winning across tabs, cut
as a copy. `copied_bodies_stay_with_their_design` gave way to the cross-design test.

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

* Clipboard: what is selected wins over a design row picked earlier

Copy copied the whole design as a linked component whenever the design's
browser row was still picked, even after a component, body or feature had
been selected in the view or another tab, and failed in an unsaved design.
The design row now gives way to anything selected, and switching tabs
drops the picked components of the design left.

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

* Put the Paste from Another Design label in the pending locale file

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

* Tests: unground the placement before moving it (a design's first part is grounded)

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

* Drop locale keys main already folded into en.json

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

* Pending locale: Paste from Another Design in the complete catalogs

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-29 16:31:09 +00:00
DuBearte 5bd85dded6 Keyboard Shortcuts: the read-only list says so and leads to the editable page (#7409) (#7449)
* claim: #7409

* Keyboard Shortcuts: the read-only list says so and leads to the editable page (#7409)

Home › Keyboard shortcuts and Help › Keyboard Shortcuts open the
Shortcuts page read-only, in the Preferences window with its sidebar and
Cancel/Apply/OK, so it looked exactly like Settings › Shortcuts with
Import…, Reset All and recording missing. The read-only page now says
"Read-only list: shortcuts can't be changed here." and has an "Edit
shortcuts" button that turns the same page into the editable one.

Strings in assets/locales/pending/shortcuts-read-only-note.json (en, ja,
zh). Harness test opens it both ways, checks the note, clicks Edit
shortcuts, and finds Import…, Reset All and a recordable binding.

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

* Pending locale: Arabic, Hebrew and Vietnamese for the read-only shortcuts note

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-29 16:30:40 +00:00
austin62jake e5ef3bad02 Viewport: with Orbit, Pan or Zoom lit, a press held still keeps the highlight under it (#8864) (#8898)
* claim: #8864

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

* Viewport: with Orbit, Pan or Zoom lit, a press held still keeps the highlight under it (#8864)

A mode lit in the navigation bar, or a held Space, owns drags, not a still
press (#8438), but the hover pick counted any left press under such a mode
as navigation and dropped the highlight, so the line under a Select Other
hold went dark until the list opened. Only a preset's left-button binding
or Ctrl+left orbit navigates from the press on now; a lit mode's drag
still drops the highlight once it starts.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 16:25:11 +00:00
github-actions[bot] 4aa2d6248e Regenerate generated files and fold pending files [skip ci] 2026-10-29 16:45:50 +00:00
github-actions[bot] bd4a234461 Regenerate generated files and fold pending files [skip ci] 2026-10-29 16:36:59 +00:00
Frank 162b3cd4ed Sketch Intersect: add Faces/Bodies and Projection Link options (#1110) (#7989)
* Sketch Intersect: add Faces/Bodies and Projection Link options (#1110)

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

* Sketch Project: add Geometry/Bodies and Projection Link options (#1110)

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

* Sketch Project/Intersect: keep exact lines and arcs of whole bodies, no noisy rebuild warning (#1110)

Whole-body silhouettes and sections fit lines and arcs tightly and snap arcs
tangent to their neighbours. A rebuilt projection warns only when it removed
the user's own constraints or curves. Pending locales cover ar, he and vi, and
the preselection tests use the direct-pick workflow.

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

* Select Other: label curves from Intersect as intersected (#1110)

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-29 16:09:18 +00:00
PaddleStroke 420dff36c8 Browser: remember collapsed state, optional reveal on hover (#8692) (#8762)
* claim: #8692

* Browser: remember the folded card across runs, optional reveal on hover (#8692)

The browser card's fold (the chevron in its header) is now a preference,
saved with the other layout state, so the next run starts the way the user
left it. Preferences > Interface > Layout adds Shown / Reveal on hover: the
browser then stays out of sight until the pointer comes over its place or
the thin tab at its left edge, fading in like the navigation bar's Hide
until hovered, and stays while the pointer is inside or a menu, drag or text
edit started from it is open. It never folds in that mode.

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

* Browser: move the reveal-on-hover messages to a pending locale file (#8692)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:03:05 +00:00
raykholo 3cdfec798f Timeline: joints in feature folders (#8306) (#8316)
* claim: #8306

* Timeline: joints in feature folders (#8306)

timeline.group and timeline.move_to_group take joints as well as features, so
hundreds of joints fold into one folder tile. A closed folder shows one tile
whatever it holds; opening it shows its features and joints in both the bottom
and left History. Suppress, delete and ungroup on a folder reach its joints too
(one undo step), deleting a joint removes it from its folder, and an unknown
joint id or name is an error. Saved with the design; designs without joint
folders read and write as before.
2026-10-29 15:59:18 +00:00
austin62jake c32f6b60eb Expressions: Enter after pi mm keeps the unit, and the list's state follows the text (#8862) (#8871)
* claim: #8862

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

* Expressions: Enter after `pi mm` keeps the unit, not a longer name (#8862)

A unit after a name or constant multiplies it, as after a number (`pi mm`
is pi millimetres), so it is a complete word there: the longer names are
listed unarmed and Enter submits what was typed. A unit at the start, or
after an operator or bracket, stays a prefix as #3664 chose.

Whether Enter completes is now worked out again every frame, unless a key
armed the list. It was kept from when the list first appeared for a word
and caret position, so it went stale when the text before the word was
replaced, or when the box took the focus before the names were gathered.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 15:56:31 +00:00
github-actions[bot] 5fe8d5a757 Regenerate generated files and fold pending files [skip ci] 2026-10-29 15:57:43 +00:00
Colin Faulkingham 4e78acc7a2 Sketch: the same constraint twice is one constraint (#5224) (#8850)
* claim: #5224

* Sketch: the same constraint twice is one constraint (#5224)
2026-10-29 15:31:18 +00:00
phanoob 4cd90e6806 Status bar: switch the mouse mapping from a menu (#7023) (#8856)
* claim: #7023

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

* Status bar: switch the mouse mapping from a menu (#7023)

The status bar gets a mouse-mapping button beside the units. It shows the
current mapping, and a click opens a menu of the mappings above the bar.
Picking one sets what Preferences > Navigation sets for it (bindings, wheel
direction, middle double-click, right drag orbits) through
choose_nav_preset. Nothing changes while the status bar is off (the
default).

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-29 15:28:34 +00:00
raykholo 8a888ff0f3 Preferences: mouse-wheel zoom speed (#4176) (#4179)
* claim: #4176

* Preferences: mouse-wheel zoom speed (#4176)

A 0.25x to 4x multiplier for each wheel zoom step, applied in
navigation::handle so every canvas uses it. The 3D mouse keeps its own
speed. Values outside the range are refused when set and reset to 1x when
loaded.

* Control protocol: wheel_zoom_speed in the sorted ui.prefs list (#4176)

* Wheel zoom speed: its label in the complete catalogs (#4176)
2026-10-29 15:26:14 +00:00
ABDULLAH 5530c3c68e Arabic: review of strings added today (Resize Part hint, navigation bar position, geometry budget help) (#8826)
* Arabic: fix the Resize Part hint (tanween style, part list, command name)

* Arabic: navigation bar Edge reads as position, tanween spelling in the geometry budget help
2026-10-29 15:24:59 +00:00
Dahak Builds 35601da3f0 3D sketch: 3D Line, Spline and Point buttons start tools (#8040) (#8041)
* claim: #8040

* wip: 3D line/spline/point tools (#8040)

* Take sketch.move3d off the toolbar help backlog now that it is agent-only (#8040)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-29 15:10:41 +00:00
github-actions[bot] 1d566398d8 Regenerate generated files and fold pending files [skip ci] 2026-10-29 15:28:09 +00:00
austin62jake 3e01081ff9 Control token: a closing window removes only its own token and discovery files (#8794) (#8837)
* claim: #8794

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

* Control token: a closing window removes only its own token and discovery files (#8794)

Under Flatpak each instance has its own loopback, so a second window
binds the same port number as the first and writes the same
<port>.token, <port>.pid and current.port in the shared data folder.
At exit the first window deleted them all, unconditionally, leaving the
second window running with no token file a client could read.

remove_token_file now takes this launch's token and removes the token
and process files only while the file still holds it; the process id
can't tell instances apart, as every Flatpak instance has the same one.
remove_current_port also takes the token and keeps the discovery file
while another launch's token file holds that port, and removes
current.pid with current.port (it was left behind).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 15:01:45 +00:00
austin62jake 420785b514 Sketch: a selected text moves when dragged from anywhere inside its frame (#8835) (#8851)
* claim: #8835

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

* Sketch: a selected text moves when dragged from anywhere inside its frame (#8835)

A drag had to start on one of the text's letters to move it. From the
space between letters, or inside a letter's outline, it found no geometry
and drew a selection box instead.

Off the sketch's own geometry, a drag from inside the selected text's
frame now grabs the text, as one from a letter does. Handles, dimensions
and geometry under the pointer still come first, and outside the frame a
drag from empty space still draws a selection box.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 14:58:03 +00:00
raykholo dcae86e456 Trackpad: zoom with two-finger scroll while holding a key you choose (#8322) (#8584)
* Trackpad: zoom with two-finger scroll while holding a key, chosen in Preferences (#8322)

Holding the zoom key turns a vertical two-finger scroll (or a wheel) into zoom at
the pointer, with the wheel zoom's speed, direction and anchor, so a laptop user
zooms without pinching. Preferences > Navigation > "Zoom with two-finger scroll
while holding": Cmd on macOS and Ctrl elsewhere (default), Option/Alt, Ctrl, or
Off. That key with the scroll did nothing before, so no default changes; Off
keeps it that way. Shift keeps its orbit.

* Trackpad zoom modifier: off by default (#8322)

Scroll keeps its usual meaning until a key is chosen in Navigation.

* Trackpad zoom modifier tests: Off is the default; tests that zoom choose Cmd (#8322)

* Trackpad zoom modifier: zoom once alongside the Ctrl+wheel pinch from #8539 (#8322)

A wheel the chosen key claims is no longer also read as a pinch, and a key that is not
chosen keeps main's pinch handling unchanged.
2026-10-29 14:49:19 +00:00
Colin Faulkingham 443e5cd59d Feature editor: allow visibility changes while editing (#8810) (#8827)
* claim: #8810

* fix(engine): preserve feature editors across visibility changes (#8810)
2026-10-29 14:26:24 +00:00
github-actions[bot] b4799575fd Regenerate generated files and fold pending files [skip ci] 2026-10-29 14:58:12 +00:00
Charlie 5a0abb8710 feat(align): add Components mode to Align for occurrence placement (#7224)
Align gets an Object Type row (Bodies | Components). In Components mode it runs the new
occurrence.align command, which moves only the chosen occurrence placements (their
transforms) instead of adding an Align feature to the shared component, so other instances
stay where they are.

- occurrence.align: from/to points, snaps, planar faces and planes in world coordinates,
  flip, capture/pending, grounded and rigid-group refusals as occurrence.move.
- A viewport or browser-body pick takes the placement directly under the active component.
- A placement inside a subassembly placed more than once still moves (as occurrence.move
  does) but the dialog warns, and the reply lists it under shared.
2026-10-29 14:19:05 +00:00
Andrzej Błasik cb05e1e0f7 GPU path tracer: run on Metal on macOS (#7183) (#7185)
* claim: #7183

* GPU path tracer runs on Metal on macOS, not only Vulkan (#3217)
2026-10-29 13:58:49 +00:00
jeromehzl 6fdd58dec6 Toasts: the corner stack keeps its full width just left of a pane at the right edge (#8804) (#8840)
corner_free looked only in a fixed column at the view's right edge and took
the largest free rect there by area. Next to a tall dialog docked at the
right edge, or the Assistant in the Floating layout, the narrow strip beside
the pane won: the toasts were squeezed into it, their text broke inside
words, and with a wider pane they were not shown at all.

It now tries that column first and then columns ending at each obstacle's
left edge, nearest the corner first, and takes the first free rect that is
TOAST_WIDTH wide and tall enough for a toast, as the frame-rate readout
moves beside a right pane (#6755); failing that, a rect tall enough beats a
wider one too short to show any toast. Without a pane the stack stays
exactly where it was.
2026-10-29 13:58:16 +00:00
phanoob 5b0a86a5b2 Graphics: a background colour of your choice for the 3D view (#7688) (#8815)
* claim: #7688

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

* Graphics: a background colour of your choice for the 3D view (#7688)

Preferences › Interface › Theme gets a Background row: the theme's
gradient (the default, unchanged) or one colour for the whole application,
filling the view in either theme. The window keeps its own colours, a
picture of the view (`ui.render`) takes the colour too, and `ui.prefs`
sets it as `viewport_background` ([r, g, b] or null).

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-29 13:58:13 +00:00
cronos 28e26209f3 Assembly Move: drag allowed joint freedoms (#6679) (#6904)
* feat: add joint-aware assembly Move handles and live motion readouts

* test: improve render job failure diagnostics

* fix: preserve assembly Move compatibility and viewport theme colors

* ci: skip default label sync for fork PRs

* fix: keep Windows CI checks portable after main rebase

* test: cover RTL assembly strings and stable refraction job

* i18n: cover assembly Move in Vietnamese

* i18n: cover assembly Move in French

* fix: complete CGA palette for assembly motion handles

* fix: complete Assembly Move palettes and Turkish strings

* fix: retain assembly move cache answers and toolbar help

* style: format merged assembly Move navigation guard

* fix: align Assembly Move preferences and tests with main

* Fix offline update fixture synchronization and clipped prism intersections

Wait for all v5 manifest requests before deploying v6. Intersect hole-free prism sections over their common height, retaining the established B-rep path for newly clipped perforated sections. Preserve the failing property seed and add closed-form and cap-boundary regressions.
2026-10-29 13:57:31 +00:00
github-actions[bot] 986de7ec9a Regenerate generated files and fold pending files [skip ci] 2026-10-29 14:25:59 +00:00
github-actions[bot] 7c7c0a57bd Regenerate generated files and fold pending files [skip ci] 2026-10-29 14:16:56 +00:00
tr1pp4 54b07d3e0b History in the browser: mark features that failed or built with a warning (#8592) (#8593)
* claim: #8592

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

* History: error and warning marks in the browser and on folded folders (#8592)

In the History "In browser" layout, a feature whose build failed gets the
error icon at the end of its row, one that built with a warning the
warning icon, with the message in the tooltip: the marks and text of the
History on the left. A folded History or Features folder carries the
worst mark of what it holds, and so does a folded folder in the History
on the left. Suppressed and rolled-back features are not built and get
none.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:51:20 +00:00
P'tite Tête 7dc341ae55 fix: Remove two per-frame quadratic lookups on large designs (#8843)
- active_body: take every body's owner in one pass (model_body_components) instead of searching the timeline body by body
- Timeline strip: find each chip's result in a table built once per frame (result_index) instead of a linear search per chip
- 4,880-body design: UI frame 31-32 ms -> 7 ms of CPU; tests pin the active body and the result lookup

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-29 13:49:59 +00:00
Ryan 5e0e168ae9 Properties: moment of inertia, principal axes and first moments of a part or assembly (#8095) (#8103)
* claim: #8095

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

* Properties: moment of inertia, principal axes and first moments of a part or assembly (#8095)

component.properties and occurrence.properties report each closed body's
inertia tensor about its centre of mass, and in total the tensor about the
total centre of mass (world axes, g·mm²), its principal moments and axes,
the first moments, and with `about: [x, y, z]` the tensor about any point.
The tensor comes from each body's welded export mesh, tessellated once per
body, scaled to the measured mass and turned by each placement; bodies add
by parallel axes. The maths is a new crates/geom inertia module.

The Properties window shows the first moments, the tensor, the principal
moments and axes, and the view marks the centre of mass with the principal
axes while it is open. Inspect → Measure → Properties now opens that window
(it ran the command and showed nothing).

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

* Properties: Copy matrix, and the whole design's card titled just Properties (#8095)

Copy matrix, under the tensor, puts it on the clipboard as three lines of
tab-separated values at full precision, in the units shown. The card for
the whole design is titled Properties; one opened on a component,
placement or bodies keeps its name.

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

* Properties: measure beside the window, so a large assembly doesn't freeze it (#8095)

Properties ran on the window thread: 18 s on a 144-body design, with the
window answering nothing meanwhile (#5203). In the desktop app the card now
measures on a copy of the session on a worker, the way #4614 made agents'
measures work, says "Measuring…" until the reply is in, and Close drops it.
A quick measure still shows at once; headless, tests and the web measure in
place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:47:44 +00:00
bherbruck be8bd43a18 CI: wasm compile in the merge queue, dependency advisories on dependency changes (#8652)
The wasm compile ran only by hand, so #8227 (a native-only DroppedFile::bytes() call in
crates/ui-egui) passed PR CI and the merge queue and broke the web build on main (#8534).
A merge group that touches the crates below the apps, the web app, the vendored crates, the
manifests, the toolchain or this workflow now runs `cargo xtask wasm`, and `ci` requires it.

Dependency advisories ran only nightly. A pull request or merge group that changes Cargo.lock
or a Cargo.toml now runs `cargo deny --locked check --warn unmaintained advisories`, and `ci`
requires it then. The nightly keeps the full advisories, bans and sources scan.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:39:48 +00:00
raykholo 884c47496b Render preview: geometry budget from the device, readable refusals (#4127) (#4154)
* claim: #4127

* Render preview: geometry budget from the device, readable refusals (#4127)

The Render preview refused every scene over a fixed 128 MiB of geometry. On
desktop the budget now follows the device's buffer limit (eight of its
largest buffers, at most 2 GiB; 2 GiB with the usual 256 MiB buffers). The
web keeps 128 MiB.

The product upload runs under its own GPU error scopes: an allocation the
device refuses is reported in the preview instead of reaching the device's
uncaptured-error handler, and the half-uploaded batches are dropped. Buffer
uploads map the buffer themselves so a refused buffer fails the upload
rather than panicking in create_buffer_init.

Refusals name the geometry size and the limit and say how to get under it,
on a theme panel card that stays readable on any preview background.

* Render preview: keep the previous geometry budget behind a preference (#4127)

* Render preview: adapt the device budget to main's GPU upload errors (#4127)
2026-10-29 13:33:39 +00:00
Karl Von Richter b2d727f4b2 Toolbar: choose visible tabs while preserving all defaults (#5717) (#8594)
* claim: #5717

* ui(feat): let users choose visible toolbar tabs

Keep Sheet Metal and Weldments visible by default while adding persisted tab visibility controls and restoring specialty tabs for designs that use them.

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

* ui(fix): respect hidden toolbar tabs in every design

Remove feature-based visibility overrides for Sheet Metal and Weldments. Keep all tabs visible by default and preserve the user choice across feature creation, opening designs, selection and undo.

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

---------

Signed-off-by: Karl Von Richter<karl@embeint.com>
2026-10-29 13:32:21 +00:00
bherbruck 4f5ed7bfa6 CI: open an issue when the nightly fails, close it when it passes (#8651)
The nightly failed every day from 2026-10-16 to 2026-10-28 without anyone noticing. A new
nightly-alert job (scheduled runs only, the only job with issues: write) opens or comments on
one issue, "Nightly CI is failing" (label ci), when a nightly job fails, and closes it when a
nightly passes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:31:29 +00:00
raykholo f03d0a20bd Update now installs the newest release, checked when pressed (#7724) (#7725)
* claim: #7724

* Update now installs the newest release, checked when pressed (#7724)

* Update: close the unmanaged-install test after the rebase (#7724)

* Update: the refresh messages in Arabic, Hebrew and Vietnamese too (#7724)
2026-10-29 13:29:43 +00:00
SilentWolf75 c6c91e6f43 Parameters: show full names in the table and formula popup (#8533) (#8551)
* claim: #8533

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parameters: show full names in the table and formula popup.

Hover tips carry the full identifier when Name cells or completion rows clip, and the completion list widens for long suggestions (#8533).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 13:25:57 +00:00
github-actions[bot] 2f56d77e90 Regenerate generated files and fold pending files [skip ci] 2026-10-29 13:45:31 +00:00
Dylan M. Taylor 68b53a4c77 Release: Arch Linux packages for x86_64 and aarch64 (#8576)
A new arch job repackages each release tarball as
solidcraft-<version>-linux-<arch>.pkg.tar.zst with makepkg, through the Craft apps' shared
action (storytold/craft-common-actions/actions/arch-package, pinned by commit). The package
is io.tangencylabs.solidcraft, named after the tarball's .desktop file, provides solidcraft
and conflicts with solidcraft-bin/-git. Each package also gets a version-less copy, and
docs/downloads.md links them.
2026-10-29 13:17:34 +00:00
phanoob 2235389344 Navigation bar: can sit at the top of the view (#8183) (#8793)
* claim: #8183

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

* Navigation bar: can sit at the top of the view (#8183)

Preferences > Navigation bar gets an Edge choice, Bottom (default) or Top.
On top the bar sits under the ribbon, its menus open downwards and its
tooltips show below it; the OK/Cancel pair and the axis view move down
below it when they would overlap.

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

* Navigation bar: the edge's labels in the complete catalogs (#8183)

ar, he, ja, vi and zh carry every English message; the Edge row's three
labels were missing from them, so `xtask i18n --check` and
`every_complete_catalog_carries_every_english_message` failed.

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-29 13:16:51 +00:00
jeromehzl 9509348320 Viewport: editing a material's colour redraws the bodies that use it (#8764) (#8814)
body_appearance_key hashed only the name of a body's assigned material, so
after material.edit changed that material's colour (or an undo or redo
changed it back) the key stayed the same and the viewport kept the cached
old look. The key now also hashes the material's resolved look; with an
unchanged material the cache is reused as before.
2026-10-29 13:14:15 +00:00
Colin Faulkingham fd8ddc23b7 Pattern on Path: reopen saved patterns in the editor (#8784) (#8805)
* claim: #8784

* fix(ui): restore saved path pattern editor
2026-10-29 12:49:17 +00:00
Joseph 07d692ee0d Keyboard Shortcuts: several keys for one command (#6273) (#6288)
* Keyboard shortcuts: a command can have several keys

Click a key on the Keyboard Shortcuts page to replace it, or the + after it
to add another; every key runs the command and menus show the first. One
key is still saved as a string, several as a list. Taking a key over from
another command leaves that command its other keys. ui.shortcut takes a
slot and reports every key.

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

* Keyboard shortcuts: follow main's search layout; fix prefs test for key lists

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-29 12:32:19 +00:00
github-actions[bot] 00f9243c0d Regenerate generated files and fold pending files [skip ci] 2026-10-29 12:29:52 +00:00
ABDULLAH 1e1be6934f Arabic: use the Mouse mapping term in the Right drag orbits help (#8803)
nav.right_drag_orbits.help now names the setting as prefs.mouse_mapping does (تعيين أزرار الفأرة).
2026-10-29 11:58:59 +00:00
semmelrinde 6a183cce02 Sketch corners: a corner cut is refused where two lines lie in line, as rounding is (#8802)
Chamfer at a corner of two lines lying on each other cut both back and
added a line of no length between their ends; lines in line with each
other have no corner either. chamfer now refuses a corner angle of 0 or
180 degrees with Fillet's message, "the lines are parallel" (found by a
local trim fuzzer).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:49:31 +00:00
jeromehzl dd6a91533d Standard parts: Resize Part says what it works on, Parts opens Insert Part (#8735) (#8801)
* Resize Part: say what it works on instead of a parameter error; Parts opens the library (#8735)

Resize Part (parts.resize) started with no standard part selected, as the
ribbon starts it, ran the command with no parameters and showed
"`component` must be an id or a name". It now says to select an inserted
standard part, and that Scale resizes a body. Parts (parts.library) only
listed the library, so its button did nothing: it opens the Insert Part
dialog. The German and Dutch button labels said "scale" ("Teil skalieren",
"Onderdeel schalen"), like the Scale command: they now say change size.

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

* Resize Part: the German label matches the command, and the test checks that nothing ran (#8735)

The German ribbon label reads "Bauteilgröße ändern", the name the command,
its dialog and its messages already use. The hint for a missing standard
part is now one function shared by the ribbon start and the Browser's
Change Part Size. The regression test also checks that the design and its
history stay as they were and no message carries the command's own error,
that a scripted parts.library still only lists the library, and that with a
standard part picked Change Part Size opens on it.

---------

Co-authored-by: semmelrinde <124386289+semmelrinde@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:49:25 +00:00
Christian Hammacher 93d0298d25 Browser: Turn Up Axis on a placement row asks that placement whether it can move (#8786) (#8788)
A placement row's menu starts from its component's menu, which takes
Turn Up Axis's enabled state from the component's first placement.
Move/Copy was already set again for the placement right-clicked; Turn Up
Axis and its two turns now are too, so a free second placement can turn
while the first is grounded, and a grounded one can't.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:28:19 +00:00
semmelrinde cf86f18c11 Sketch: an end held on a removed curve where another curve ends stays tied (#8779)
A trimmed circle ended on a line exactly where an arc started on that
line, held there by a point-on-curve tie. Trimming the line away dropped
the tie and left the two ends on one spot, untied. remove_curves now ties
each point held on a removed curve at one of its ends to that end before
the curve goes (found by a local trim fuzzer, 15 -> 12 cases in 2000
seeds).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:07:28 +00:00
github-actions[bot] 63f053e4fe Regenerate generated files and fold pending files [skip ci] 2026-10-29 11:06:21 +00:00
Colin Faulkingham 81039a0e10 Pinch zoom: route Ctrl+wheel input to the camera (#8480) (#8539)
* claim: #8480

* Pinch to zoom on touchpad: native Ctrl+wheel pinch zooms the view (#8480)

The viewport's pinch path read only `egui::Event::Zoom`; `immediate_wheel` leaves
wheel events carrying the zoom modifier to egui. Native Linux and Windows
touchpads report a pinch as Ctrl+wheel in points, so the gesture reached neither
path and the view did not move. `pinch_factor` now reads those wheels too, in
every wheel unit and scaled as egui scales them, and `ui.scroll` can send the
same gesture (`ctrl: true`) so scripts and scenarios can replay a pinch.

* fix: finish Ctrl+wheel pinch handling and replay validation (#8480)
2026-10-29 10:40:10 +00:00
Lars Jansen 375dc01602 Navigation: Right drag orbits option; picking the SolidWorks preset turns it off (#6492) (#6497)
* claim: #6492

* feat(navigation): Right drag orbits option, off when picking the SolidWorks preset, which also fits on middle double-click (#6492)
2026-10-29 10:24:37 +00:00
PaddleStroke fe3f913d76 Dialogs: disable Apply until something changed (#8561) (#8728)
* claim: #8561

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

* Dialogs: disable Apply until something changed (#8561)

The dialog footer gets a disabled state for Apply (Footer::nothing_to_apply,
Window::apply_enabled): drawn with the theme's dim ink, not clickable, not
reached by Tab and reported disabled to accessibility. Preferences enables
Apply only while something differs from its snapshot (what Cancel would put
back), so it is disabled when the dialog opens and again after Apply.
ui.prefs replies with apply_enabled.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:14:45 +00:00
bherbruck 90f0353724 Release 0.26.1 (#8647)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:33:01 -04:00
N3MI-DG 4f3ac07aec Move/Copy: changing Move Type keeps the move made so far, and picks follow the moved body (#8746) (#8747)
* claim: #8746

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

* Move/Copy: changing Move Type keeps the move made so far, and picks follow the moved body (#8746)

Switching the Move Type reset the values, so the move made under the
earlier type was lost. The dialog now keeps it (`chain`) and the new type
moves the bodies on from there; params() composes the two into one
solid.move (one turn about the origin and a translation, as before).
While the new type's picks are missing, the kept move alone is made.

The new type measures from where the bodies now stand (Free Move's pivot,
Rotate's ring, round-edge centres and axes on a moved body). The picker,
the edge/face highlights, the centre marker and Rotate's axis line follow
the moving body, mid-drag too; a pick on it is taken back by the current
type's own move so it doesn't move with the move it sets. While the
triad is dragged the moved bodies' highlights are hidden and rebuilt once
on release.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:47:26 +00:00
ABDULLAH 710a3f8f11 Arabic: re-translate outdated strings and review the Render light and frame strings (#8752) 2026-10-29 09:47:18 +00:00
github-actions[bot] 4cee4db19b Regenerate generated files and fold pending files [skip ci] 2026-10-29 09:59:37 +00:00
semmelrinde 036d4234a1 Croatian: translate the 781 messages added since the catalog came in (#7813), the Render workspace included (#8683)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:29:12 +00:00
PaddleStroke 7341b8f6be Browser: Show/Hide context menu on origin axis rows (#8382) (#8724)
* claim: #8382

* Browser: Show/Hide context menu on origin axis rows (#8382)

Right-clicking an origin axis row (X, Y, Z) now opens its own menu with
Show/Hide (V), through ui.originPart and browser.visibility like the
origin planes' entry. An axis takes no sketch, so Create Sketch is not
offered.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:20:11 +00:00
semmelrinde 0f2ad19bdb Navigation menus: a label too long for its row ends in an ellipsis (#8700) (#8745)
The Display menu's "Perspective while orbiting, ortho on faces" ran past
the menu's edge: a menu row painted its label at full length. It is now
cut before the row's detail and check, and shown whole on hover, as a
long name in a list is.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:20:05 +00:00
jeromehzl 1643b74111 Sketch: selected points stay marked after the pointer moves on (#1069) (#8714)
* fix(ui): keep selected sketch points highlighted

Add persistent point selection rings while retaining constraint-state markers, with regressions for pointer selection, dialog inputs, aliases and visibility. Fixes #1069.

* Move the sketch point selection tests into the viewport overlay tests (#1069)

Run them through a frame that clears its texture uploads, as the current egui
requires, next to the other overlay tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Victor Santos Oliveira <56417388+Vittuu@users.noreply.github.com>
Co-authored-by: Colin Faulkingham <colinfaulkingham@Colins-Mac-mini.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 08:46:34 +00:00
github-actions[bot] 815e7de7e7 Regenerate generated files and fold pending files [skip ci] 2026-10-29 09:07:06 +00:00
Nathan Dane 3cc10acafa Render lights: uniform light list, solo and mute, and the Light editor (#6281) (#7265)
* Render lights: uniform light list, solo and mute, and the Light editor

* Render lights: Lighting and Light editors to the approved design

* Render lights: the scale-test frame carries the product light list

* Restore the stable Render light mute target on the shared eye control

* fix: validate the viewport BRDF probe with current shader dependencies

* fix: compile legacy lighting controls only for tests

* fix: scope the legacy lighting preset import to tests

* fix: reuse the merged Add Light label and translations

* test: enable the implemented Add Light radial action

* fix: restrict the legacy CPU vector editor to tests

* test: reveal Output before checking Lights browser focus

* test: use the published Browser card rectangle for Output scrolling

* test: reveal Output children before exercising editor navigation
2026-10-29 08:40:51 +00:00
N3MI-DG 06dfbc4000 Sketch Create menu: family flyouts open beside their row, on hover (#8267) (#8713)
* claim: #8267

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

* Sketch Create menu: family flyouts open beside their row, on hover (#8267)

A family row (Line, Rectangle, Arc, ...) opened its variants as a dropdown
below the row, covering the rows under it. The flyout now opens to the
right of its row, level with it, and on the left only when the right has
no room. Hovering a row opens it, as the context menu's Appearance
flyout does; resting on another row closes it after the shared delay.

The context menu and the Create menu share the flyout timing
(SUBMENU_CLOSE_DELAY, flyout_hover, flyout_leaving) and placement
(flyout_at) in ui_kit::menu.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:31:36 +00:00
semmelrinde 2d56f5d599 Construction: a vertex where another body's edge runs past stays its corner (#8650) (#8722)
A vertex pick found its named edges and then picked each again by a point
on it, among the edges of every body. At a corner where another body's
edge runs past (a wall against a side wall), all three became that edge,
and the vertex its end 3 mm away. Take each named edge's ends from the
body that has it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:03:31 +00:00
ABDULLAH 2f7a9a1fc0 Arabic: review of strings added since the catalog came in (#8393)
* Arabic: consistent joint type names, rim face and tanween spelling

* Arabic: review of the Render and Edit in Place strings
2026-10-29 07:58:14 +00:00
github-actions[bot] 7ad2bbd391 Regenerate generated files and fold pending files [skip ci] 2026-10-29 08:12:10 +00:00
austin62jake c10d36a0b5 Tests: Open Linked Design waits for the part as settle waits for a load (#8716)
menu_tests::linked_component_opens_its_part_in_a_tab_and_reports_a_missing_source
gave the background open of the linked part 600 frames. Headless frames
are quick, so on a loaded machine (the whole ui-egui suite at once) the
read can take longer, and the test then found the assembly still shown.
It now waits until the part is open, up to 120 s as Harness::settle and
the edit-in-place tests wait for a load.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 07:37:20 +00:00
raykholo aee57875f5 Sketch Fillet: 3D sketch lines are not offered and say they can't be filleted yet (#4160) (#4177)
* claim: #4160

* Sketch Fillet: 3D sketch lines are not offered, and say they can't be filleted (#4160)

The Fillet tool offered 3D sketch lines as picks, and sketch.fillet then
refused them with "`a` must be a line". The tool no longer offers them, and
the command refuses them with "not supported yet: filleting 3D sketch lines".

* Sketch Fillet and Chamfer: refuse all spatial sketch curves (#4160)

* Sketch corner test: give the picked curve its point (#4160)

main added a point to sketch-curve hits after this branch was written.
2026-10-29 07:22:53 +00:00
Nathan Dane d2fcab8432 Render shot editor: camera and frame controls, aspect presets, guides and frame overlay (#6281) (#7232)
* Render shot editor: camera and frame controls, aspect presets, guides and the frame overlay

* Render shot: Shot editor and Scene overview to the approved design

* Distinguish Render stage variants from native window frames

* fix: restore library inspection after clearing shot selection

* test: follow the merged Output Check navigation

* test: inspect Check without requiring a preflight result

* test: cancel Shot removal through its shared dialog action

* fix: remove obsolete Inspector stage form after editor integration

* fix: preserve Appearance Advanced when clearing Wear focus

* test: expect the dedicated Check editor from Scene overview
2026-10-29 07:03:24 +00:00
github-actions[bot] c7a1cee357 Regenerate generated files and fold pending files [skip ci] 2026-10-29 07:43:09 +00:00
semmelrinde 883f8e8e90 Save: the status line says where the design was saved (#8693) (#8705)
Saving gave no sign that it worked, while an export names the file it wrote.
After Save or Save As, the status line now reads "Saved <path>".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:48:35 +00:00
github-actions[bot] a6b272dec8 Regenerate generated files and fold pending files [skip ci] 2026-10-29 07:13:25 +00:00
github-actions[bot] 2a749f4294 Regenerate generated files and fold pending files [skip ci] 2026-10-29 07:06:44 +00:00
semmelrinde 5241c64aa6 Sketch solver: eliminate row by row on slices, so the dense solve vectorises (#8631)
The dense solve updated each matrix element through bounds-checked get/add;
on slow scripted drags it was most of the time. The same operations in the
same order on row slices give bit-identical solutions, about twice as fast.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:39:05 +00:00
raykholo c688054db0 Canvas and Decal say where to click after the image is chosen (#4359) (#4396)
* claim: #4359

* Canvas and Decal say where to click after the image is chosen (#4359)

After picking an image the placement tool started silently, so it looked
as if nothing had happened. Show the tool's placement hint as a toast while
it waits for the click (translated), and end it once the image is placed.

* Canvas: include placement hint in viewport obstacles (#4359)
2026-10-29 06:17:35 +00:00
semmelrinde d77b0434dd English (UK): UK spellings for 43 messages that still had US ones (#8696)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 06:16:27 +00:00
N3MI-DG 0818ad0ed4 Preferences: typing in a number field edits it instead of searching (#8662) (#8663)
* claim: #8662

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

* Preferences: typing in a number field edits it instead of searching (#8662)

Type-to-search takes printable keys unless the focused field notes the key
sink. number_field (Autosave every, the Largest file to open limits) never
did, so its digits went into the search.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:32:25 +00:00
github-actions[bot] 54f74a613e Regenerate generated files and fold pending files [skip ci] 2026-10-29 06:38:44 +00:00
github-actions[bot] 74791e0220 Regenerate generated files and fold pending files [skip ci] 2026-10-29 06:29:24 +00:00
raykholo afe7dcd4e1 Allow sketch Mirror to use origin axes (#4193) (#4207)
* claim: #4193

* Sketch Mirror: an origin axis can be the mirror line (#4193)

Mirror accepted only a straight curve of the sketch. It now also takes an
origin axis that lies in the sketch plane ({axis: X|Y|Z}), projected into
the sketch as a fixed linked line, so the copies stay symmetric to it and
follow the source when it moves.

* Sketch Mirror: use current sketch store (#4193)
2026-10-29 05:08:00 +00:00
semmelrinde fed7ea0c88 Malay: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8667)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:03:26 +00:00
semmelrinde dfa8ae9a60 Hungarian: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8655)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:03:00 +00:00
semmelrinde 53b2f73d2e Bosnian: translate the 781 messages added since the catalog came in (#7813), the Render workspace included (#8682)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:02:58 +00:00
austin62jake 1dce42e744 Preferences: a drop-down's Search box keeps what is typed in it (#8675)
In Preferences, letters typed in a drop-down list's own Search box (the
Font list, a 3D mouse's button assignments) went to the Preferences search
instead, which then filtered the page, usually to "No matching
preferences", and closed the list. The Preferences search takes typing only
when no text field held the keyboard on the previous frame, and the
drop-down's Search box never said it did. On the full Interface page the
number fields' focus check covered for it; once a search hid them, as when
the Font row is found by searching for "font", nothing did.

The Search box in searchable_dropdown_with now reports that it has the
keyboard, as the Preferences search box does.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 05:02:44 +00:00
semmelrinde aad3d56ec2 Serbian: translate the 781 messages added since the catalog came in (#7813), the Render workspace included (#8684)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:02:18 +00:00
semmelrinde 8719cc7b57 Swedish: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8660)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:02:06 +00:00
semmelrinde 651addda84 French: translate the 262 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8654)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:01:47 +00:00
github-actions[bot] dd16cc9faf Regenerate generated files and fold pending files [skip ci] 2026-10-29 06:02:10 +00:00
github-actions[bot] 6747ea0dcf Regenerate generated files and fold pending files [skip ci] 2026-10-29 05:53:14 +00:00
semmelrinde 4215fb11c2 Italian: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8656)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:01:40 +00:00
semmelrinde 504fe1afcb Dutch: translate the 262 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8657)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:01:37 +00:00
semmelrinde 7367b81ffe Romanian: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8659)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:01:33 +00:00
semmelrinde 183226d2b6 Turkish: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8661)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:55:04 +00:00
semmelrinde 514563187b Traditional Chinese: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8664)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:54:48 +00:00
semmelrinde 5286f50fd9 Ukrainian: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8665)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:54:42 +00:00
semmelrinde 46487216d0 Indonesian: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8668)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:54:24 +00:00
semmelrinde 39250092a3 Galician: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8669)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:54:18 +00:00
semmelrinde a1fd2be2c8 Norwegian Bokmål: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8670)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:54:11 +00:00
Nathan Dane bd8cb4e6d6 Render output: Effects and Check sections, Size presets panel and the Check chip (#6281) (#7233)
* Render output: Effects and Check sections, Size presets panel and the Check tool chip

* Render output: Sampling, Effects and Check editors as child rows under Output

* fix: restrict legacy Output controls to test builds

* fix: cover status controls and reveal Effects test targets

* fix: include shared quality reviews in copied check reports

* fix: remove the obsolete Effects GPU guard after Wear integration
2026-10-29 04:39:34 +00:00
Nathan Dane c1925bb219 Render appearance: click, drag and paint to assign, Pick Up, copy and paste (#6281) (#6431)
* Render appearance: click, drag and paint to assign, Pick Up, copy and paste, and Remove

* Exclude the workspace Shots strip from dialog width checks

* Exclude the collapsed Shots control from dialog width checks

* Retain assembly cache listings and measure actual dialog bounds

* fix: handle toast button rectangles in assignment status

* fix: restrict gallery note measurement helper to tests

* test: measure assignment help at the catalogue footer

* fix: share the command revision snapshot after restacking
2026-10-29 04:33:31 +00:00
mynoc 75035ac4a5 Color picker: add Hex alongside U8 and F (#7445) (#7450)
* claim: #7445

* Color picker: add shared Hex input mode (#7445)

Accept strict RGB/RGBA Hex input while preserving exact opacity,
opaque/PBR restrictions, invalid drafts and existing U8/F behavior.

Keep explicit English fallback entries for the three new Hex messages
in Arabic and Hebrew, which final integration main now requires as
complete catalogs. No Arabic or Hebrew translations are claimed.

The user authorized this feature-only commit after full cargo xtask ci
stopped at the pre-existing geometry nonminimal_bool lint. The same
failure was reproduced without this patch at the published claim and
main. No unrelated lint fix or suppression is included.

* i18n: cover complete catalogs for Hex picker (#7445)

Add the three Hex message keys to Vietnamese in the feature's pending
locale file. Keep the unchanged English source values as explicit
fallbacks; no Vietnamese translations are claimed.

Audit the complete-catalog set (ar, he, ja, vi, zh) rather than only
the first catalog reported by CI. Preserve the existing completeness
test and all picker/Appearance code.

The completeness regression changes from red to green. Full local CI
and wasm checks remain blocked by failures independently reproduced
in clean upstream main; no unrelated fixes or suppressions are added.

* Appearance: use the shared opacity-aware color picker in Render (#7445)

Route the merged Render Appearance editor through the UI-kit picker,
passing exact opacity only for non-PBR looks. Keep a non-editable mixed
label and treat explicitly accepting the first color as a mixed edit.

Remove the external Hex field and duplicate parser, replace its gallery
entry with the shared mixed-aware picker, and update the control contract.

Preserve existing regression assertions and add actual Render UI tests
for alpha edits, precision, invalid drafts, PBR rejection and mixed edits.

Full local CI and wasm were run before this commit and remain blocked
by the separately reproduced unchanged upstream geometry lints and
prefs.bytes wasm error. No unrelated fixes or suppressions are included.

---------

Co-authored-by: mynoc <298906611+mynoc-96@users.noreply.github.com>
2026-10-29 04:33:10 +00:00
github-actions[bot] ae47ed342b Regenerate generated files and fold pending files [skip ci] 2026-10-29 05:24:00 +00:00
Mike111177 03cf8f3e90 Configuration table: a filter in the Add Column menu (#8086) (#8088)
* claim: #8086

* Configuration table: a filter in the Add Column menu (#8086)

The + menu listed every parameter, feature suppression and body
material/appearance with no way to narrow it. A filter field now sits at
the top, takes focus when the menu opens, narrows the entries and the Face
Appearance submenu as typed, picks a lone match on Enter and clears when
the menu closes. A click into the field keeps the menu open (plus_header
closes on outside clicks; entries close it themselves), and the list keeps
its full height after the filter has emptied it once.

New kit element ui_kit::menu_filter (glyph, frameless editor, rule) so the
field does not read as an entry; gallery, i18n guard and docs/ui-kit.md
updated. Regression test in manage_dialogs_tests.
2026-10-29 04:20:42 +00:00
MarkusRe 0baf49b25f Windows: never load a dxcompiler.dll found on PATH; FXC unless DXC ships beside the app (#6756) (#7923)
* claim: #6756

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

* Windows: never load a dxcompiler.dll found on PATH; FXC unless DXC ships beside the app (#6756)

wgpu's Dx12Compiler::Auto loads dxcompiler.dll by name, so Windows takes the
first one on PATH. SolidCraft ships none, and DXC 1.8.2505 from other programs'
folders (Obsidian, NVIDIA SDK Manager) crashed every start with
EXCEPTION_ILLEGAL_INSTRUCTION during the viewport shader compile.

solidcraft_gpu::dx12_compiler() now chooses: WGPU_DX12_COMPILER as wgpu reads
it when it names a compiler; otherwise DXC from the dxcompiler.dll beside the
program, by its full path, else FXC. The window, the headless/CLI instance and
the test helpers start with it, and dx12_compiles_with_fxc follows it.
WGPU_DX12_COMPILER=dxc keeps the old lookup.

Fixes #6756

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:14:57 +00:00
github-actions[bot] 9bcda52408 Regenerate generated files and fold pending files [skip ci] 2026-10-29 04:45:23 +00:00
semmelrinde f4e52c4d3e Sketch: points tied only through a removed curve's end stay tied (#8640)
* Sketch: points tied only through a removed curve's end stay tied

Removing a curve removed its orphaned end points with their constraints, so
two points made coincident with such an end lost their tie although they
still met: two rectangles' corners on one arc end came apart when the arc
was trimmed. They are now tied to each other before the point goes.

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

* Sketch: keep the tie test with the other trim tests

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:14:22 +00:00
semmelrinde 2da969cf5c Czech: translate the 264 messages the catalog lacks, Render panels and point clouds included (batch 6) (#8653)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:14:04 +00:00
NOWN-DEV dd8308e835 Naming: a face swept from one of many sketch curves is named without measuring every curve (#8643)
Naming a face swept from a sketch flattened every curve of the sketch to
a polyline and measured its distance to each, for every face. On a sketch
with thousands of curves (a sheet with many cut-outs) that grew with
curves times faces. The curves are now flattened once per feature, each
with its box, and a curve whose box is farther than the best distance so
far is not measured.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 04:12:56 +00:00
Colin Faulkingham 6f9788ccba Browser: move a component under another by drag and drop (#5164) (#8532)
* claim: #5164

* Browser: drag a placement row onto another to move it into that component (#5164)

---------

Co-authored-by: Colin Faulkingham <colinfaulkingham@Colins-Mac-mini.local>
2026-10-29 04:01:21 +00:00
github-actions[bot] 46a6686380 Regenerate generated files and fold pending files [skip ci] 2026-10-29 04:15:14 +00:00
CaRLymx ec76068a0b Spanish (es-ES): catch up with en.json (#6899)
* Spanish (es-ES): catch up with en.json

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

* Navigation preferences: name the Shift key through the catalog

Spanish now translates the key name, so the page painted it in English.

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-29 03:42:24 +00:00
Colin Faulkingham 99664a4d35 fix: keep expression move distances on world axes (#7234) (#8508) 2026-10-29 03:39:39 +00:00
froglemonade2 e55eff7c5d Orbit about the cursor: on an edge or sketch line, turn about the point under the cursor (#7356) (#7358)
* claim: #7356

* Orbit about the cursor: on an edge or sketch line, turn about the point under the cursor (#7356)

With Orbit around: point under the cursor, a drag that starts on a body
edge turned about the edge's middle, which on a long edge is far from
the cursor, and one that starts on a sketch curve turned about the
centre of the bodies in view. Edge and sketch curve hits now carry the
point under the cursor, which the pick already measured, and
pivot_under uses it. An edge hit's `mid` is unchanged, so selections
still record the edge's middle.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 03:36:05 +00:00
austin62jake d8d22cd52a 3MF export: a component's bodies as one object with parts (#8139) (#8187)
* claim: #8139

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

* 3MF export: a component's bodies as one object with parts (#8139)

file.export and file.save_mesh wrote every body as its own 3MF object and
build item, so a slicer loaded a multi-body part as separate objects and
could arrange them apart on the bed. With objects: "component", each placed
component that holds two or more bodies is written as one object whose
<components> are its bodies' meshes, built once, so PrusaSlicer, OrcaSlicer,
Bambu Studio and Cura load it as one object with a part per body, each of
which can take its own filament. The design's own bodies, and a component
with a single body, stay objects of their own. The default (objects: "body")
writes the same file as before.

- io: model_xml_parts and write_3mf_parts write the parts; export_with_parts
  maps body names to them. model_xml_with and export_with_quality give the
  same output as before.
- engine: mesh_parts reads `objects` (component is refused for formats
  other than 3MF); component_parts groups the placed bodies by placement,
  each part named after its occurrence (Bracket:1, Bracket:2).
- file.save_mesh passes the parts on when it also sends the file to a slicer;
  file.send_to_slicer's own folder export is unchanged.

Fixes #8139.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 03:35:38 +00:00
Nathan Dane 40ff251e5f Render wear: a Wear row and editor for the aging layers (#6281) (#7459)
* Render wear: a Wear row and editor for the aging layers

* test: read Wear header text from accessibility label values

* test: restore body picks for Wear header recovery assertions
2026-10-29 03:29:10 +00:00
github-actions[bot] 081c4bca73 Regenerate generated files and fold pending files [skip ci] 2026-10-29 03:40:32 +00:00
Nathan Dane de5cee9bf1 Viewport status item: UI-kit element and row (#8544) (#8553)
* claim: #8544

* Add reusable viewport status items and fixture (#8544)
2026-10-29 03:02:43 +00:00
Nathan Dane 09bf603588 Render environment editor: rotation, intensity, backdrop colour and ground (#6281) (#7231)
* Render environment editor: rotation, intensity, backdrop colour and ground through render.environment.set

* Render environment editor: rotation ring in the view with one undo step per drag

* Render environment: Source, orientation, backdrop, ground and atmosphere sections

* Keep segmented controls inside their allocated rows

* fix: compile legacy environment controls only for tests
2026-10-29 02:49:16 +00:00
semmelrinde f071f00155 Sketch Trim: an end at a place several curves cut is tied to the one with an end there (#8612) (#8628)
Where several curves cut at one place, trim pinned the new end onto the
first of them. At the touch point of two circles that was a rectangle side
passing through, and once a later trim removed the side the arc ends were
loose: a trimmed dog bone kept two untied ends.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 02:45:35 +00:00
raykholo 8fb452b05e Report a bug: always offer a way to submit (#5463) (#5615)
* claim: #5463

* Report a bug: always offer a way to submit (#5463)

* Keep report strings pending and update the focus regression (#5463)
2026-10-29 02:43:49 +00:00
semmelrinde e4cfbbd0c3 Sketch Coincident: refuse a line's two ends, as MidPoint refuses its own end (#8611)
* Sketch Coincident: refuse a line's two ends

Making a line's two ends coincident shrinks it to a point; a Parallel or
Perpendicular on it afterwards has no direction to hold and a later solve
could throw points far away while reporting solved, as with a line's own end
as its midpoint (#8562).

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

* Sketch Coincident: keep the own-ends test with the other Coincident tests

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 02:21:05 +00:00
Colin Faulkingham 746e1c633d Dialogs: wrap status and refusal messages at full width (#8455) (#8460)
* claim: #8455

* fix(ui): wrap dialog status and refusal messages (#8455)
2026-10-29 02:16:56 +00:00
The Nomad 674ef84b53 Add History playback to the Inspect ribbon (#3693) (#7853)
* claim: #3693

* Fix timeline playback to show each rebuilt feature

* Move History playback into the Inspect ribbon

* Refuse Play History up front when playback couldn't run

Starting Play and the per-frame check that stops it were looking at
different app state, so Play could be accepted during an apply, a delete
check or queued history work and then stop silently on the next frame.
Both now use one playback_blocked check.

The UI also reads the engine's PlaybackAction as the typed enum instead of
matching strings, so a bad reply is an error rather than a quiet toggle.

* Put Play History in the History controls

History now shows five controls in transport order: start, previous,
play, next, end. Play runs timeline.play, so it turns into Stop while
playback runs, same as the ribbon button and the shortcut.

It's on by default (#3688, #3740, #4216). Anyone who wants the old four
controls back can right-click them and pick Hide Play Button, or clear
history_play_button under Preferences > Interface. In a named vertical
History too narrow for five buttons beside its grip, Play is left to the
ribbon rather than overlapping the grip.

* Draw the History Play button as a plain play/pause glyph

The Play button in the History controls now uses the same drawn glyphs as
its neighbours: a filled triangle, and two bars while History plays. The
triangle is bigger than the step arrows so it doesn't read as Forward.
The ribbon command keeps its timeline_play and timeline_stop icons.

* Keep the playback changes clear of PRs queued ahead

Several PRs in the merge queue touch the same lines, so this moves ours
aside without changing behaviour:

- history_play_button and its reset-test row sit with the other History
  preferences, not beside view_confirm_corner (#7248 adds status_bar there).
- History keeps main's history_transport_width() for four controls (#6430
  and #7459 call it); the Play button's count uses history_controls_width.
- A new or opened document stops playback after dim_view::cancel(), clear
  of the line #7819 adds after reset_chat.
- The Play/Stop label swap moves from interactive_command_info, which #7906
  rewrites, into the ribbon's command_info.

* Stop playback after the command completes, clear of #6433

#6433 rewrites the session.execute line in execute_command, and our playback check sat right under it, so the queue couldn't merge the two. Checking the revision after command_completed does the same job a couple of lines later.

---------

Co-authored-by: cfaulkingham <colin.faulkingham@gmail.com>
2026-10-29 02:14:53 +00:00
Frank be757154d6 fix(sketch): join nearby profile endpoints (#7269)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-29 01:54:56 +00:00
PaddleStroke 3b983dc550 Viewport: vertex markers where edges are drawn (#7834) (#8119)
* claim: #7834

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

* Viewport: vertex markers where edges are drawn (#7834)

Each body's vertices (the ends of its pickable edges, once each) show as
small screen-space squares in the edge colour in the styles that draw
edges: shaded with edges, wireframe, and shaded with hidden edges, where
hidden vertices show faintly like hidden edges. Plain shaded draws none.
A marker is a lifted line from the vertex to itself in the body's block,
so it is instanced with the edges, cached with them, and depth-tested
like a highlight band. The existing vertex hover ring stays on top.

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

* Viewport tests: count edge segments apart from vertex markers (#7834)

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

* Vertex markers: an option, off by default (#7834)

Adds the Vertex markers preference (Preferences > Graphics, off by default) and draws the dots only when it is on.

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-29 01:53:01 +00:00
github-actions[bot] b5217b1e86 Regenerate generated files and fold pending files [skip ci] 2026-10-29 03:07:55 +00:00
github-actions[bot] 5dccc3d978 Regenerate generated files and fold pending files [skip ci] 2026-10-29 03:00:55 +00:00
semmelrinde 800b6b4486 Saved model: read text records up to 16 MiB, so a long warning keeps fast opening (#8541) (#8607)
Open refused a saved model whose text records (a feature's warning, a body or
sketch name) held more than 65,536 bytes, although Save writes them, so such a
design was evaluated again on every open.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:52:21 +00:00
semmelrinde 11f208f665 Viewport: cut dashed lines to the view, so a huge distance can't fill the memory (#8540)
* Viewport: cut dashed lines to the view, so a huge distance can't fill the memory

epaint makes one shape per dash and steps along a line in f32. The
viewport's dashed lines (the Extrude taper handle, Measure's legs, the
fillet radius guide, the inference and snap guides, a sketch text's
frame) were dashed over their whole projected length. With an Extrude
distance of 1e9 mm the taper line was billions of points long: the
frame never finished and filled the memory within a second.

`viewport::dashed_line` dashes a line that lies in view exactly as
before, and cuts one that reaches far off screen to the view (grown by
two dash periods) first, keeping its dashes in phase with its start.

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

* Viewport: compute a cut dashed line's ends in f64

A line a trillion points long lost its cut ends in f32: they collapsed to one point, and the part in view drew nothing. The new test caught it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:27:34 +00:00
phanoob ba52632985 Tabs: Ctrl+Tab shows the next design tab, Ctrl+Shift+Tab the previous one (#8565) (#8597)
Two new commands, view.next_tab and view.previous_tab, with Ctrl+Tab and
Ctrl+Shift+Tab as their default keys: they appear on the Keyboard Shortcuts
page and can be rebound or cleared like any other. The window steps along the
tab strip and goes round at either end; one design alone stays as it is, and
a "save changes?" prompt keeps its tab shown. The engine only echoes the
step (no design edit, no undo step); engine.batch refuses them like the
other steps that go to another design.

Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:25:12 +00:00
bherbruck 4571701179 Embed a Windows version resource in solidcraft.exe (#8378)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 01:21:02 +00:00
raykholo 4c6382c0a3 PR checks: a read-only token gets a notice from the label job, not a failure (#7850) (#7851) 2026-10-29 01:00:15 +00:00
raykholo 0e01dc9bad Pattern counts: say when a parameter has a length unit, and refuse fractional counts (#5066) (#5337)
* claim: #5066

* Pattern counts: name a length-unit parameter, refuse fractional counts (#5066)

A count parameter created with a length or angle unit now gets an error
that names it and says to set its Unit to none. A count that evaluates to
a fraction is refused instead of being rounded silently.
2026-10-29 01:00:10 +00:00
bherbruck c0d84ad460 Dependencies: security updates (2026-10-28) (#8588)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:30:53 +00:00
yhamil04 af9772fb3c Browser: mesh bodies get their own icon and a body-kind tooltip (#6907) (#7059)
A mesh body's row (an imported STL, 3MF, OBJ, ...) now shows a new
mesh_body icon, a triangulated cube in the mesh accent, instead of the
solid body's grey cube. Hovering a body row says Mesh body, Surface body
or Solid body. Body::is_mesh() decides; nothing new is saved.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:26:01 +00:00
w0prup 3449f0def2 Wayland: trackpad pinch zooms the view (#7744) (#8023)
* claim: #7744

* Vendor winit 0.30.13 with Wayland touchpad pinch and rotation gestures

The 0.30 Wayland backend never binds zwp_pointer_gestures_v1, and
WindowEvent::PinchGesture / WindowEvent::RotationGesture only come from the
macOS and iOS backends, so a trackpad pinch produces no input at all on
Linux and Wayland (two-finger scroll still works, since X11/XWayland
reports it as a wheel). egui-winit already maps the events to
egui::Event::Zoom and navigation::handle already zooms the camera from
that, so the Wayland backend was the only missing piece.

Upstream the gesture support is part of winit 0.31 (rust-windowing/winit#4338),
which eframe 0.36 cannot use while it still requires winit 0.30, and the
0.30 backport rust-windowing/winit#4778 was closed unmerged. So ship the
same fix locally, upstream-style-applied and stripped of example and test
targets, with its unit tests for the gesture state machine kept under
cargo test -p winit --lib -- pointer_gesture:

- src/platform_impl/linux/wayland/seat/pointer/pointer_gesture.rs: bind
  the global (versions 1..=3), one pinch gesture object per pointer,
  destroyed before the pointer is released; begin/update/end become the
  two events for the window the gesture began over. Two fingers only;
  the protocol's scale is relative to the gesture's beginning, so the
  event carries the change since the previous update; rotation is
  negated to winit's counterclockwise degrees; a non-finite scale is
  dropped, and so are updates of a gesture whose begin we never saw.
- seat state wiring: bind the global, track and drop the gesture object.
- wayland-protocols' unstable feature, platform notes on both events,
  changelog, patch-section entry in Cargo.toml, vendor/README, and
  Linux/Wayland notes in the troubleshooting guide and the UI tour.

Retire this vendoring once an eframe release consumes winit 0.31 or
later; the cleaned branch is kept as
https://github.com/purp0w/winit/tree/wayland-pinch-gestures.

* ci: retrigger after flaky occurrence-cache test

---------

Co-authored-by: Francisco das Chagas Danta Filho <dev@dantas-filho.me>
2026-10-29 00:09:02 +00:00
github-actions[bot] 0889ef9dfc Regenerate generated files and fold pending files [skip ci] 2026-10-29 02:11:11 +00:00
semmelrinde 1937dea04b Sketch constraints: refuse one curve picked twice for a two-curve constraint (#8577)
Parallel, collinear, equal or concentric of a curve with itself was added
as a constraint that always holds; perpendicular or tangent to itself was
refused as geometry that can't move. These six now refuse one curve
picked twice, as Coincident already does for a point, and leave the
sketch as it was. Constraints already in a design are untouched.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:52:44 +00:00
Nathan Dane a545f32113 Render stage panels: Surfaces and references, Studios and environments, and the review dialogs (#6281) (#7999)
* Render stage panels: Surfaces and references, Studios and environments, and the review dialogs

* fix: complete stage panel translations and gallery tile fields

* fix: exercise stage catalog sizing in the floating layout

* fix: retain the Appearance library selection query
2026-10-28 23:49:39 +00:00
github-actions[bot] 937d5f0176 Regenerate generated files and fold pending files [skip ci] 2026-10-29 01:43:00 +00:00
semmelrinde 05a7a4fef9 Traditional Chinese: translate the 56 messages main gained since batch 1 (batch 3) (#8365)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:28:02 +00:00
N3MI-DG 33c27106ff Pick by the body's own size, not the camera's distance (#7919) (#7920)
* claim: #7919

* Pick by the body's own size, not the camera's distance (#7919)

The occlusion test that decides whether an edge or vertex is hidden by the
face in front allowed a slack of `scene_radius(app) * 0.02`, and
`scene_radius` ends with `.max(app.cam.half_height() * 2.0)`: in an
orthographic view that is `0.016 x camera distance`. Zooming out grew the
slack without bound until a whole body fitted inside it and every edge behind
the front face counted as visible, and edges and vertices are offered before
faces, so they won the pick. Cutting the slack from the scene also gave a
small part in a large assembly a tolerance many times its own size.

`pick_slack` now takes a size and returns the mesh's own chord tolerance with
room to spare, `DisplayQuality::Coarse.chord(size) * 4.0`: a facet sits at
most a chord tolerance inside its surface, and that tolerance is already a
fraction of the body's size, so the slack no longer moves when the camera
does. Each body is tested with its own bounding box, which the loop already
holds. `view.pick` does the same, so an agent's pick still matches a click on
the same pixel. Sketch profiles and origin planes belong to no body and keep
a scene-level value, `scene_pick_slack`.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-28 23:15:09 +00:00
semmelrinde 17807ab6dc Sketch MidPoint: refuse a line's own end as its midpoint (#8562)
A line's own end held at its middle shrinks the line to a point. The
solve accepted that, and a later angle, tangent or parallel on the line
had no direction to hold: its points went millions of millimetres away
while the sketch still reported solved. MidPoint now refuses the pick,
saying why, and leaves the sketch as it was. Constraints already in a
design are untouched (only the command refuses).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:11:05 +00:00
momysnow 1445b5a4f0 Extend: pick two curves to extend both to where they meet (#4750) (#8392)
* claim: #4750

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

* Extend: pick two curves to extend both to where they meet (#4750)

`sketch.extend` gains a second form: `curves: [a, b]` names two lines or
arcs, with an optional `at: [[x,y],[x,y]]` naming the end of each to grow
(default: the end nearest the meeting point). Both grow along their own
line or circle to where they cross and the two ends are joined with one
Coincident, in one undo step. `moved` is the joined end's id.

- Where two crossings are possible (line and arc, two arcs) the one with
  the least total growth is used; a tie is refused.
- A curve that already reaches the point does not grow. An end held by
  other geometry or a constraint stays and gets a new line or arc.
- Parallel or collinear lines, curves that never meet, a point behind the
  chosen end, the same curve twice, splines, ellipses, infinite lines,
  offset geometry and malformed `curves`/`at` are refused with no change.
- The single-curve form is unchanged; its code is shared through
  `grow_line` / `grow_arc`.

Canvas: in the Extend tool, Shift-click two curves. Shift goes through
the tool modifier registry as the new `extend_pair` action, and Extend
turns off Shift's midpoint snap. Escape drops a pending first pick. The
sketching guide describes the gesture.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 23:10:24 +00:00
github-actions[bot] 066de1b005 Regenerate generated files and fold pending files [skip ci] 2026-10-29 01:07:21 +00:00
github-actions[bot] d6876d6e67 Regenerate generated files and fold pending files [skip ci] 2026-10-29 00:58:24 +00:00
raykholo a1e7337d3c Toasts: one stack in the view's bottom-right corner, one width, buttons as in dialogs (#7893) (#7959)
* claim: #7893

* Toasts: one stack in the view's bottom-right corner, one width, buttons as in dialogs (#7893)

* Control protocol: toast_place in the sorted ui.prefs list (#7893)

* Toasts: a sketch test clears the status notice before clicking the view's corner (#7893)

The test's line end lands in the bottom-right corner, where notices now stack
and take clicks as they did at the bottom centre; the test is about the circle.

* Toasts: the pattern scenario's circle sits clear of the view's corner (#7893)

After the refit, a circle at y 0 landed in the bottom-right corner, where the
tool's prompt now stacks and takes clicks. The circle moves 40 mm up; the
pattern it checks is the same.
2026-10-28 22:35:24 +00:00
semmelrinde 951b5eb1d3 Indonesian and Traditional Chinese: the Appearances panel's 27 Render messages (#8336)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:27:28 +00:00
Colin Faulkingham 5221103369 Components: a component named like the design is reachable by name (#8358) (#8423)
* claim: #8358

* Components: a component named like the design is reachable by name (#8358)

Document::find_component resolved the design's own name to the root component (id 0) before
searching the component list, so a component that shares the design's name was unreachable by
name: appearance.assign reported `no component `CurtainRailPlug`` (the same lookup serves every
command that takes a component by name).

An exact component name (or component id) match now wins over the design-name fallback; "0" and
"root" still always mean the design.

Regression test: a_component_named_like_the_design_takes_its_appearance_by_name (fails on main
with the reported error, passes with the fix).
2026-10-28 22:07:58 +00:00
JK3D 9b7a6ea04d History: feature folders get their own symbol (#8521) (#8522)
* claim: #8521

* feat: give History feature folders their own symbol

A folder of features in the History on the left drew the same folder icon
as the Browser's folders. It now draws feature_group, a pile of history
chips with the front one in blue, and so do Add to New Folder and Remove
from Folder in the History's menu. Other uses of folder keep it.
2026-10-28 22:04:12 +00:00
Lars Jansen fe265d74b1 Sketch: draw conflicting curves and points in the conflict colour (#7244) (#7249)
* claim: #7244

* feat(sketch): draw curves, points and constraints that conflict in their own colour (#7244)
2026-10-28 22:03:54 +00:00
NOWN-DEV eb29c4b2d8 Sheet metal feature commands: typed parameter contracts (#6581) (#6582)
* claim: sheet.* typed parameter schemas

* Sheet metal feature commands: typed parameter contracts

sheet.flange, sheet.perforate, sheet.hem, sheet.fold, sheet.unfold, sheet.refold and
sheet.convert read their parameters by hand, so a misspelt key or a value of the wrong
shape was ignored or failed late, and describe_command had only prose for them.

Each now has a contract in schema/sheet.rs, checked in front of the unchanged handler,
and returns FeatureResult. The values no shared type fits (sheet-edge points, an edge
flange's edges, a fold's line and fixed point, the flange type, the positions, the
relief shape and the perforation pattern) get types that call the handlers' own
readers, split out of cmd/sheet.rs, and give their messages. The rule's values
(thickness, K-factor, bend radius, reliefs) stay out of the contracts: the handlers
refuse them, and strict refuses them as unknown keys.

A test passed a nested `base` object whose `sketch` the flange never reads; it now
names the sketch at the top level.

Part of #2829 (#6581).

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

* Sheet metal contracts: a further bend's angle stays as lenient as before

The flange handler reads a `then` bend's `angle` that is no expression
(`true`, a list) as left out and bends it 90 degrees. The rebased
contract had moved that check into the shared reader, so the handler
refused such a call even with the check off.

The reader is back to the handler's behaviour, and `ThenSteps` is the
stricter one, as the schema guide asks: warn reports such an angle and
strict refuses it. Tests cover the reader's leniency, the type's
refusal and the three check levels on one flange.

Part of #2829 (#6581).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:01:35 +00:00
github-actions[bot] f1ab19422b Regenerate generated files and fold pending files [skip ci] 2026-10-29 00:30:57 +00:00
github-actions[bot] 0a187f5cb1 Regenerate generated files and fold pending files [skip ci] 2026-10-29 00:21:59 +00:00
tr1pp4 7adbe00377 History in the browser: a History layout that lists each component's history in the browser (#7709) (#7710)
* Browser: the history in the browser, as a History layout (compact mode)

Settings > Interface > History gains "In browser" (view.history_layout dock: browser):
neither timeline is shown, and each component's History folder lists its sketches and
features in history order with the marker. Dragging the marker rolls the history back
through timeline.roll_to; Find in Timeline reveals the feature in the browser.

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

* Browser history: sketches listed under their feature as in the vertical History

With Sketches: Under their feature, the history in the browser (compact mode) lists each
sketch under the first feature built from it, through the shared history_rows (#6494)
without reference rows: same rule, same fold key, so switching the History layout keeps
what is open. A drop above a row rolls back its listed sketches with it. Selecting a feature
lights the rows of its sketches, and selecting a sketch lights the features built from it.

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

* Browser: with the history in the browser, the Sketches folder starts closed

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

* Browser history: sketch rows have the eye of their Sketches folder row

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

* History in the browser: Arabic, Hebrew and Vietnamese for the layout's name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:49:21 +00:00
semmelrinde 0243afdc0b Dialogs: scroll a long refusal into view with its Create Issue (#8454) (#8506)
* Dialogs: scroll a long refusal into view with its Create Issue (#8454)

A dialog's refusal comes last in its fields. When the fields fill the
dialog, the refusal sits under the fold: an Extrude Cut that the kernel
refused showed only its first line, "boolean Cut: the operation failed: no",
and no Create Issue. The dialog now scrolls a new refusal into view once:
a refusal on OK at once, a preview's once no field is being typed in, so a
field never moves away while it is typed in.

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

* Dialogs: decide the reveal in one function and test it

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:48:09 +00:00
raykholo 8e6a107f6e Lists many PRs add to: no written lengths, one file per theme (#7747) (#7898)
* Lists many PRs add to: no written lengths, one file per theme (#7747)

* xtask lists: the fixed-table allowlist in order, each with its own reason

* Lists: the wide-character blocks and the drawing scale presets as slices too (#7747)

* Lists: the drawing units test reads UNITS as a slice (#7747)
2026-10-28 21:33:34 +00:00
semmelrinde 4ad18407dc Paste: leave copied body transfers out, so the design still opens (#8478)
A copied body transfer was pasted like any feature and given the target
component as its owner. A transfer belongs to the component it moves bodies
to, so the saved design was refused on open ("body transfer owner must match
its destination"), and a pasted transfer had nothing to move anyway: the
copies go into the component they are pasted into. Paste now leaves body
transfers out and says so in its notices; a clipboard of only transfers is
refused.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:29:05 +00:00
semmelrinde 5695785e3e Dutch: translate the 117 messages the catalog still lacks (batch 4) (#8486)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:26:12 +00:00
semmelrinde c2e1dbe6e4 Italian: translate the 125 messages the catalog still lacks (batch 4) (#8491)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:26:00 +00:00
Logan Night bda5f69c5c Preferences: reverse trackpad orbit direction (#8410) (#8484)
* claim: #8410

* Preferences: reverse trackpad orbit direction (#8410)

Add "Reverse trackpad orbit direction" under the pan settings in
Preferences > Navigation. orbit_reverse defaults off, so older settings
files load unchanged, and turns the Shift + two-finger orbit the other
way on both axes, as natural scrolling flips both. Pan, zoom and mouse
orbiting are untouched. Labels in en, ar, de, he, ja, ko, vi and zh as
a pending locale file; orbit_reverse in the control protocol's sorted
settings list.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:25:24 +00:00
semmelrinde 78d05e35bb Malay: translate the 125 messages the catalog still lacks (batch 4) (#8492)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:25:18 +00:00
semmelrinde 70fdc27703 Czech: translate the 121 messages the catalog still lacks (batch 4) (#8485)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:24:15 +00:00
semmelrinde b650d7b49b French: translate the 121 messages the catalog still lacks (batch 4) (#8487)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:24:12 +00:00
semmelrinde 883aff8816 Traditional Chinese: translate the 125 messages the catalog still lacks (batch 4) (#8498)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:23:58 +00:00
semmelrinde 43cf7774aa Galician: translate the 125 messages the catalog still lacks (batch 4) (#8488)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:23:54 +00:00
raykholo 29749c73f3 Report missing translations for complete catalogs before review (#8542) (#8543)
* claim: #8542

* CI: report missing translations for complete catalogs before review (#8542)

`cargo xtask i18n --check` already reads the pending locale files and fails
when a complete catalog lacks an English message, but no workflow ran it, so
the gap surfaced only in the full test run after approved-for-ci. It now runs
in Format and repository checks, and CONTRIBUTING names the complete catalogs.
2026-10-28 19:51:47 -04:00
github-actions[bot] 05081e8428 Regenerate generated files and fold pending files [skip ci] 2026-10-28 23:50:48 +00:00
1222 changed files with 242530 additions and 13215 deletions
+5 -1
View File
@@ -1,6 +1,8 @@
<!-- Title: which part of the app changes and what a user now sees or can do, clear to someone who hasn't seen your chat, the issue or the code. -->
## Change
Describe the problem and resulting behavior. Link the issue when applicable.
Start with what this is, in plain words someone new to the project would understand: what it does and who it's for. Don't assume anyone knows the name of your tool, project or feature. Then describe the problem and resulting behavior. Link the issue when applicable.
Changes a default: no
@@ -14,6 +16,8 @@ What did you run, and what were the results? Include expected and measured value
## Screenshots of the behavior (when observable)
Put the picture or video here in the description. A file in the diff doesn't count: a maintainer has to be able to see what this is without opening the changed files.
For observable CAD, UI or model behavior, include actual descriptive screenshots or a short recording from a small reproduction. This includes kernel, geometry, assembly and backend effects that can be shown in the app. Coding agents should run the app and capture the evidence themselves, rather than asking a human for evidence they can produce. Show the actual failure/result, preferably before and after, with brief steps, expected/actual behavior and annotations where helpful.
Flowcharts and Mermaid diagrams cannot substitute for evidence of visible behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate the point. Changes with no useful observable behavior may omit screenshot/video evidence. Exclude private designs and sensitive details; attach media or link to an accessible recording rather than committing generated review artifacts.
+113 -6
View File
@@ -15,7 +15,9 @@
# nightly. The reference tests and the doc tests run in the merge queue, on main and nightly, not
# on pull requests (the fast lane, in Select checks); a break there is fixed forward on main. The
# Windows jobs (D3D12 shaders and app startup) run nightly, and in the merge queue only for a group
# that changes what they exercise. Only main saves the build caches; pull requests restore them.
# that changes what they exercise. The wasm compile check runs in the merge queue for a group that
# touches the crates below the apps, the web app or the manifests. Dependency advisories run nightly
# and on any pull request or merge group that changes Cargo.lock or a Cargo.toml. Only main saves the build caches; pull requests restore them.
# sccache is on a Blacksmith sticky disk that only main commits.
# A push to main runs only the repository checks when a passing merge-queue run tested that very
# commit, or when nothing a build reads changed since the last push to main whose run passed.
@@ -78,8 +80,13 @@ env:
jobs:
dependency-advisories:
# Nightly and by hand: advisories, bans and sources for everything. A pull request or merge
# group that changes Cargo.lock or a Cargo.toml (Select checks' `deps`) checks the advisories
# only, so a dependency with a known vulnerability is caught before it lands, not the next
# morning; other pull requests skip it. `ci` requires it whenever `deps` is true.
name: Dependency advisories
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs: changes
if: ${{ !cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || needs.changes.outputs.deps == 'true') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -88,7 +95,12 @@ jobs:
- name: Install advisory scanner
run: cargo install cargo-deny --version 0.20.2 --locked
- name: Check locked dependencies
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: cargo deny --locked check --warn unmaintained advisories bans sources
# deny.toml denies unmaintained crates; like the nightly, they are reported, not fatal.
- name: Check advisories for the changed dependencies
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
run: cargo deny --locked check --warn unmaintained advisories
changes:
name: Select checks
@@ -115,6 +127,10 @@ jobs:
packages: ${{ steps.paths.outputs.packages }}
oracle: ${{ steps.paths.outputs.oracle }}
doctests: ${{ steps.paths.outputs.doctests }}
# Cargo.lock or a Cargo.toml changed (pull requests and merge groups): Dependency advisories.
deps: ${{ steps.paths.outputs.deps }}
# A merge group reaches the wasm build: Compile WebAssembly.
wasm: ${{ steps.paths.outputs.wasm }}
# The passing run a merge of main carries forward (its URL), when this push is only that.
carried: ${{ steps.paths.outputs.carried }}
steps:
@@ -143,6 +159,8 @@ jobs:
goldens=false
windows=false
gpu_paths=false
deps=false
wasm=false
full=true
packages=
# Whether the NUL-separated paths in $1 reach any code: scripts/ci/affected.py selects
@@ -183,6 +201,21 @@ jobs:
esac
done < "$1"
}
# Whether the NUL-separated paths in $1 change the dependencies (Cargo.lock or any
# Cargo.toml: deps), and whether they reach what `cargo xtask wasm` compiles (wasm): the
# crates below the apps, the web app, the vendored crates, the manifests and lockfile, the
# toolchain and cargo config, and this workflow. #8227 put a native-only call in
# crates/ui-egui and the merge queue passed it, because nothing built for wasm there.
deps_wasm_paths() {
while IFS= read -r -d '' path; do
case "$path" in
Cargo.lock|Cargo.toml|*/Cargo.toml) deps=true ;;
esac
case "$path" in
crates/*|apps/solidcraft-web/*|vendor/*|Cargo.lock|Cargo.toml|rust-toolchain*|.cargo/*|.github/workflows/ci.yml) wasm=true ;;
esac
done < "$1"
}
# Opt-in for a big render change: a pull request labelled render-tests, or a merge group
# holding one (its pull requests are the (#N) of its squash commits), runs them whatever
# the paths say.
@@ -228,14 +261,17 @@ jobs:
if git diff --no-renames --name-only -z "$GROUP_BASE_SHA...$GROUP_HEAD_SHA" > "$RUNNER_TEMP/ci-paths"; then
windows_paths "$RUNNER_TEMP/ci-paths" "$GROUP_BASE_SHA...$GROUP_HEAD_SHA"
gpu_paths "$RUNNER_TEMP/ci-paths"
deps_wasm_paths "$RUNNER_TEMP/ci-paths"
if [ "$gpu_paths" = false ] && group_has_render_label "$GROUP_BASE_SHA..$GROUP_HEAD_SHA"; then
gpu_paths=true
fi
else
windows=true
gpu_paths=true
deps=true
wasm=true
fi
echo "windows=$windows gpu_paths=$gpu_paths for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
echo "windows=$windows gpu_paths=$gpu_paths deps=$deps wasm=$wasm for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
elif [ "$EVENT_NAME" != pull_request ]; then
# The nightly run and Run workflow: everything, Windows included.
native=true
@@ -256,6 +292,10 @@ jobs:
done < "$RUNNER_TEMP/ci-paths"
# Computed as for a merge group, then turned off below (the fast lane).
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
# The advisories run on the pull request itself (it builds nothing of ours: about two minutes, most of it installing cargo-deny);
# the wasm compile waits for the merge queue.
deps_wasm_paths "$RUNNER_TEMP/ci-paths"
wasm=false
# Pull requests build and test the touched packages and their dependents; nothing
# selected means no code changed.
select_packages "$RUNNER_TEMP/ci-paths"
@@ -327,6 +367,8 @@ jobs:
echo "windows=$windows"
echo "oracle=$oracle"
echo "doctests=$doctests"
echo "deps=$deps"
echo "wasm=$wasm"
echo "carried=$carried"
echo "full=$full"
echo "quick=$quick"
@@ -364,6 +406,9 @@ jobs:
cargo test -p xtask ui_containers --locked
- name: Sorted lists
run: cargo xtask lists
# New UI text must reach every complete catalog; report it here, before review (#8542).
- name: Translations in the complete catalogs
run: cargo xtask i18n --check
- name: Agent router and skills cite real files
run: |
cargo xtask skills
@@ -943,7 +988,7 @@ jobs:
# commit's real run instead: the merge queue reads `ci` from the newest run of a commit, so a
# run without a passing `ci` would block a pull request that passed (#1850, #3182, #3252).
name: ci
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup]
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup, dependency-advisories, wasm]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
@@ -997,6 +1042,10 @@ jobs:
ORACLE: ${{ needs.oracle.result }}
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
WINDOWS_STARTUP: ${{ needs.windows-startup.result }}
DEPS_RUN: ${{ needs.changes.outputs.deps }}
ADVISORIES: ${{ needs.dependency-advisories.result }}
WASM_RUN: ${{ needs.changes.outputs.wasm }}
WASM: ${{ needs.wasm.result }}
run: |
test "$CHANGES" = success
test "$CHECKS" = success
@@ -1026,6 +1075,14 @@ jobs:
test "$WINDOWS_FXC" = success
test "$WINDOWS_STARTUP" = success
fi
# Cargo.lock or a Cargo.toml changed on a pull request or in a merge group.
if [ "$DEPS_RUN" = true ]; then
test "$ADVISORIES" = success
fi
# A merge group that reaches the wasm build.
if [ "$WASM_RUN" = true ]; then
test "$WASM" = success
fi
- name: Note a carried result
if: needs.changes.outputs.carried != ''
env:
@@ -1033,9 +1090,13 @@ jobs:
run: |
echo "This push only merges main into a change that already passed; the heavy checks were not repeated. Passing run: $CARRIED" >> "$GITHUB_STEP_SUMMARY"
# Every crate below the apps and the web app must build for the browser (`cargo xtask wasm`, a
# cargo check, no tests). In the merge queue for a group that reaches the wasm build (Select
# checks' `wasm`), where `ci` requires it; by hand with cross_compile.
wasm:
name: Compile WebAssembly (manual)
if: github.event_name == 'workflow_dispatch' && inputs.cross_compile
name: Compile WebAssembly
needs: changes
if: ${{ !cancelled() && ((github.event_name == 'workflow_dispatch' && inputs.cross_compile) || needs.changes.outputs.wasm == 'true') }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
steps:
@@ -1096,3 +1157,49 @@ jobs:
. "$HOME/.cargo/env"
export CRAFT_FONTS_DIR="$PWD/craft-fonts"
cargo check --all-targets --locked --target riscv64gc-unknown-linux-gnu
# Nobody watches the nightly run, and it failed every day from 2026-10-16 to 2026-10-28 before
# anyone noticed. When a nightly fails, open one issue, "Nightly CI is failing" (label `ci`), or
# comment on it if it is already open; when a nightly passes, close it. A nightly that neither
# failed nor fully passed (cancelled) leaves the issue as it is. The only job that may write issues.
nightly-alert:
name: Nightly alert
needs: [dependency-advisories, changes, checks, ci, build-tests, action-matrix, action-matrix-determinism, property-tests]
if: always() && github.event_name == 'schedule'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
permissions:
issues: write
steps:
- name: Open, update or close the nightly issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RESULTS: ${{ toJSON(needs) }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
SHA: ${{ github.sha }}
run: |
title="Nightly CI is failing"
failed=$(jq -r 'to_entries[] | select(.value.result == "failure") | .key' <<<"$RESULTS")
unfinished=$(jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "failure") | "\(.key) (\(.value.result))"' <<<"$RESULTS")
issue=$(gh issue list --state open --label ci --limit 100 --json number,title \
--jq "[.[] | select(.title == \"$title\")] | first | .number // empty")
if [ -n "$failed" ]; then
body="The nightly run on ${SHA:0:9} failed: $RUN_URL
Failed jobs: $(paste -sd , - <<<"$failed" | sed 's/,/, /g')."
if [ -n "$issue" ]; then
gh issue comment "$issue" --body "$body"
else
gh issue create --title "$title" --label ci --body "$body
This issue is opened and updated by the nightly run (the nightly-alert job in .github/workflows/ci.yml) and closed by the first nightly that passes."
fi
elif [ -z "$unfinished" ]; then
if [ -n "$issue" ]; then
gh issue close "$issue" --comment "The nightly run on ${SHA:0:9} passed: $RUN_URL"
fi
echo "The nightly passed."
else
echo "Nothing failed, but not every job finished: $unfinished. The issue is left as it is."
fi
+4 -3
View File
@@ -1,6 +1,7 @@
# 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).
# While a new geometry kernel is evaluated, new issues and discussions about kernel work, and new
# pull requests that change the kernel directories, 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:
+19 -3
View File
@@ -1,6 +1,6 @@
# 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).
# Three quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
# for an issue a lower-numbered one already closes, the "Changes a default:" line is filled in, and
# a PR adding to the showcase shows a picture. None is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
name: PR checks
on:
@@ -48,10 +48,26 @@ jobs:
const r = await checks.checkDefaults({ github, context, core });
core.info(JSON.stringify(r));
picture:
name: Picture 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.checkPicture({ 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
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
timeout-minutes: 5
continue-on-error: true
+44 -4
View File
@@ -10,6 +10,7 @@
# (see the windows job). Nothing else is signed yet.
#
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
# Arch Linux x86_64, aarch64 .pkg.tar.zst, repackaged from the .tar.gz the arch job
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
# 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
@@ -206,6 +207,45 @@ jobs:
path: dist/release/*.flatpak
if-no-files-found: error
arch:
name: Arch Linux ${{ matrix.arch }}
needs: [version, linux]
if: needs.version.outputs.build == 'true'
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
env:
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-linux-${{ matrix.arch }}
path: dist/linux
# The Craft apps' shared Arch packaging (storytold/craft-common-actions, MIT OR Apache-2.0):
# makepkg in an archlinux:base-devel container. The package is named after the tarball's
# .desktop file (io.tangencylabs.solidcraft) and written as
# dist/release/solidcraft-<version>-linux-<arch>.pkg.tar.zst.
- uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main
with:
tarball: dist/linux/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.tar.gz
arch: ${{ matrix.arch }}
- name: Check the package
env:
PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst
run: |
tar --zstd -tf "$PKG" | grep -E '^usr/bin/solidcraft$|^usr/bin/solidcraft-cli$|^usr/share/applications/io\.tangencylabs\.solidcraft\.desktop$'
tar --zstd -xOf "$PKG" .PKGINFO | grep -E '^(pkgname|pkgver|arch) = '
tar --zstd -xOf "$PKG" .PKGINFO | grep -qx 'pkgname = io.tangencylabs.solidcraft'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-arch-${{ matrix.arch }}
path: dist/release/*.pkg.tar.zst
if-no-files-found: error
snap:
@@ -310,7 +350,7 @@ jobs:
name: macOS build ${{ matrix.arch }}
needs: version
if: needs.version.outputs.build == 'true'
runs-on: macos-14
runs-on: macos-15
strategy:
fail-fast: false
matrix:
@@ -344,7 +384,7 @@ jobs:
name: macOS universal
needs: [version, macos-build]
if: needs.version.outputs.build == 'true'
runs-on: macos-14
runs-on: macos-15
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
@@ -571,7 +611,7 @@ jobs:
release:
name: GitHub Release or dry-run artifacts
needs: [version, linux, flatpak, snap, freebsd, macos, windows, web]
needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web]
if: needs.version.outputs.build == 'true'
runs-on: ubuntu-latest
permissions:
@@ -597,7 +637,7 @@ jobs:
shopt -s nullglob
for f in "solidcraft-$VERSION-"*; do
case "$f" in
*.msi | *-portable.zip | *.dmg | *.AppImage | *.deb | *.rpm | *.flatpak | *.snap)
*.msi | *-portable.zip | *.dmg | *.AppImage | *.deb | *.rpm | *.flatpak | *.snap | *.pkg.tar.zst)
cp -- "$f" "solidcraft-${f#"solidcraft-$VERSION-"}" ;;
esac
done
+1
View File
@@ -22,6 +22,7 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
- Show observable changes with real screenshots or a recording, captured yourself.
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
+157 -4
View File
@@ -84,6 +84,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| assets/fonts/craft-ui-sc/CraftUI-SC.otf | Adobe / Google Inc. (Noto Sans SC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/SC Regular, glyph-subset for assets/locales/zh.json and renamed Craft UI SC (OFL reserved-name); licence text in assets/fonts/craft-ui-sc/OFL.txt; web UI Simplified Chinese | SIL OFL 1.1 |
| assets/fonts/craft-ui-tc/CraftUI-TC.otf | Adobe / Google Inc. (Noto Sans TC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/TC Regular, glyph-subset for assets/locales/zh-TW.json and renamed Craft UI TC (OFL reserved-name); licence text in assets/fonts/craft-ui-tc/OFL.txt; web UI Traditional Chinese | SIL OFL 1.1 |
| tests/step-conformance/cone_point.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/cone_point_vertex.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/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
@@ -125,6 +127,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| 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/craft-ui-sc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-SC.otf | SIL OFL 1.1 |
| assets/fonts/craft-ui-tc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-TC.otf | 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 |
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
@@ -185,11 +189,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-dark.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-dark-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-dark-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/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 |
@@ -603,6 +603,158 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/icons/ui/design/svg/model/finish_edit.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| `assets/locales/pending/sketch-tracking-guides.json` | SolidCraft contributors | Original alignment-guide preference labels and help text | MIT OR Apache-2.0 |
| assets/locales/pending/point-clouds.json | SolidCraft contributors | Original point-cloud UI strings and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/feature_group.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/mesh_body.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/timeline_play.svg | SolidCraft contributors | Original history playback icon; canonical grey_cube base and original action cue | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/timeline_stop.svg | SolidCraft contributors | Original history playback icon; canonical grey_cube base and original action cue | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/status_pin.svg | SolidCraft contributors | Original analytic pin icon for persistent viewport results | MIT OR Apache-2.0 |
| assets/locales/pending/render-wear.json | SolidCraft contributors | Original UI labels and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/equation_curve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/locales/pending/sketch-recent-snap.json | SolidCraft contributors | Original preference messages for the recent centre and midpoint snap | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_horizontal.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_vertical.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_angle.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_parallel.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_perpendicular.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_external.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_at.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_tangent_two.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/construction_line_bisector.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-circle.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-circle.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-flower.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-flower.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-involute.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-involute.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-line.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-line.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-parabola.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-parabola.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-spiral.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-spiral.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/equation-wave.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-wave.json`; gitignored) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/print_face.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/locales/fi.json | SolidCraft contributors | Original Finnish translation of SolidCraft's English catalog, AI-assisted (Claude Code) clean-room from the English and not yet reviewed line by line by a native speaker; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_axis_point.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-2x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-2x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-4x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-4x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-8x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-8x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-off.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/aa-off.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/agent-pointer-speed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/agent-pointer-speed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/ao-on.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/ao-on.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/assembly-move.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/assembly-move.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/browser-card.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/browser-card.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/browser-column.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/browser-column.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-bends.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-bends.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-draft.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-draft.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-failed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-failed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-fixed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-fixed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-projected.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/colour-vision-projected.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/direct-interaction.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/direct-interaction.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/frame-rate.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/frame-rate.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-bottom.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-bottom.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-in-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-in-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-left.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-left.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-under-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/history-under-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/layout-schematic.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/layout-schematic.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/measure-axis-distances.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/measure-axis-distances.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/mouse-mapping.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/mouse-mapping.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/navigator-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/navigator-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/navigator-cube.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/navigator-cube.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-centre.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-centre.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-origin.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orbit-origin.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orientation-y-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orientation-y-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orientation-z-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/orientation-z-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/quick-measure.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/quick-measure.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/select-bodies.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/select-bodies.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/select-faces-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/select-faces-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-conflict.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-conflict.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-keep.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-keep.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-previous.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-previous.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-unless-turned.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-exit-unless-turned.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-guides.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-guides.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-hide-glyphs.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-hide-glyphs.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-state-icons.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/sketch-state-icons.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/style-shaded-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/style-shaded-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-coarse.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-coarse.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-fine.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-fine.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-normal.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-normal.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-ultra.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tess-ultra.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-dark.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-dark.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-gruvbox.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-gruvbox.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-imported.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-imported.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-light.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-light.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-system-dark-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-system-dark-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-system-light-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/theme-system-light-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-extrude.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-extrude.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-hole.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-hole.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-placement.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/tool-placement.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/vertex-markers.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/vertex-markers.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-angles.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-angles.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-grid.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-grid.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-perspective.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-perspective.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-shadow.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| docs/design/preferences/figures/view-shadow.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
## Code adapted from sibling projects
@@ -662,6 +814,7 @@ copied into SolidCraft.
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
+33 -8
View File
@@ -19,12 +19,13 @@ Questions, ideas or want to say what you're working on? Join the [Discord](https
## 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.
We're moving to a new geometry kernel. Until that's done, please don't open pull requests that
change `crates/kernel/`, `crates/geom/` or `vendor/truck*`, or issues and discussions about kernel
work: booleans, fillets, chamfers, shell, offsets, sweeps and lofts, threads and holes, STEP/IGES
import and export, or tessellation. 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. Pull requests outside those directories carry on as normal,
including the dialogs and tools for these features.
## Get it running (about 10 minutes on a fast machine)
@@ -117,7 +118,8 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
not.) Permissive
dependencies are fine. Note which one you're adding and why in the PR. THIRD-PARTY-LICENSES.txt
lists it once the change lands: the Generated files workflow rewrites it on `main`.
Nightly/manual CI also scans advisories, bans and sources with cargo-deny; see
Nightly/manual CI also scans advisories, bans and sources with cargo-deny, and a PR that
changes Cargo.lock or a Cargo.toml checks the advisories; see
[the local command and coverage limits](docs/book/src/dev/testing.md#dependency-advisories).
3. **Every asset is original or openly licensed and has a row in
[ATTRIBUTION.md](ATTRIBUTION.md).** Icons are drawn in code (`crates/ui-egui/src/icons.rs`).
@@ -278,6 +280,11 @@ Some files gain an item in many pull requests at once. They are kept so that two
each add an item only meet when their items are neighbours, which spares everyone a conflict
and a fresh CI run:
- **Growing Rust lists**: use slices (`const ITEMS: &[Item] = &[...];`) without a written
length. `cargo xtask lists` rejects non-test const/static arrays with more than ten lines
of entries, except fixed standard or algorithm tables listed with reasons in `xtask/src/lists.rs`.
Built-in themes need only a JSON file in `assets/styles/ui/themes/`, with a unique nonnegative
integer `order` (the picker order) and its own `provenance` line; keep existing order values.
- **Message catalogs** (`assets/locales/*.json`): one message per line, sorted by id. Add your
message anywhere and run `cargo xtask lists --fix`; `cargo xtask lists` (in `cargo xtask ci`
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
@@ -300,7 +307,9 @@ and a fresh CI run:
- **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).
`cargo xtask fold-pending` folds them on `main` (see the README in each folder). A new English
message also needs every catalog delivered complete (`COMPLETE` in `crates/ui-egui/src/i18n.rs`:
today ar, he, ja, vi, and zh) in the same pending file; `cargo xtask i18n --check` reports any gap.
## Sending a change
@@ -330,6 +339,15 @@ and a fresh CI run:
PRs in groups by area, so PRs that touch the same code meet a conflict at most once.
4. Reference the issue (`Fixes #12`). In the PR, say what you tested and how. For geometry work, include the measured numbers (and
the expected ones).
5. Write the PR title and first sentence for someone who hasn't seen your chat, the issue or the
code. Maintainers pick what to review from the title, and it becomes the commit on `main`. Say
which part of the app changes and what a user now sees or can do, and name things rather than
leaning on "there", "it" or "this".
- Unclear: `Send to Slicer: pick one print face per body there, and show print faces only while it is open`
- Clear: `3D printing: choose the face each body rests on in the Send to Slicer window, not the face menu`
Use plain, literal English in PR text: one idea per sentence, active voice, and one word for one
thing. This helps everyone, including readers whose first language isn't English.
Commenting `/claim` assigns you, adds the `in progress` label, and replies with its automatic cleanup
deadline. It frees a claim with no linked PR after 72 hours, or a claim with a PR after 14 days
@@ -338,6 +356,13 @@ without pushes. These automatic cleanup deadlines are separate from the daily pr
`/unclaim` releases the bot claim early (only you or a maintainer can). Issues a maintainer has
taken are never freed automatically.
**A PR has to explain itself.** Start the description with what it is, in plain words someone new
to the project would understand: what it does and who it's for. Don't assume anyone knows the name
of your tool, project or feature. If there's anything to look at, show it in the description: a
picture or video has to be visible there without opening the changed files. "The GIF is the change"
doesn't count. A PR that doesn't do this waits until it does. For a PR that adds to
`docs/showcase.md` or `docs/screenshots/`, the Picture check fails until the description shows one.
Help reviewers see what the words mean. For observable CAD, UI or model behavior, please
include actual descriptive screenshots or a short recording in the issue or PR. This evidence
is required for changes or bugs that can be demonstrated in the app, including kernel,
Generated
+43 -25
View File
@@ -60,8 +60,6 @@ dependencies = [
[[package]]
name = "accesskit_macos"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
dependencies = [
"accesskit",
"accesskit_consumer 0.38.0",
@@ -3773,9 +3771,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
[[package]]
name = "rustls-webpki"
version = "0.103.15"
version = "0.103.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
dependencies = [
"ring",
"rustls-pki-types",
@@ -4210,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4235,21 +4233,23 @@ dependencies = [
"serde_json",
"solidcraft-3dx-client",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
"solidcraft-ui-egui",
"ureq",
"windows-sys 0.61.2",
"winit",
"x11rb",
"zbus 5.19.0",
]
[[package]]
name = "solidcraft-3dx-client"
version = "0.26.0"
version = "0.30.1"
[[package]]
name = "solidcraft-cli"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"image",
"log",
@@ -4261,7 +4261,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4270,7 +4270,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"log",
"serde",
@@ -4287,7 +4287,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"image",
"log",
@@ -4308,7 +4308,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"serde",
"serde_json",
@@ -4317,7 +4317,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"image",
"log",
@@ -4337,7 +4337,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4355,13 +4355,14 @@ dependencies = [
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-testkit",
"thiserror 2.0.21",
"zip",
]
[[package]]
name = "solidcraft-kernel"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"log",
"proptest",
@@ -4381,8 +4382,9 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"image",
"serde_json",
"solidcraft-engine",
"solidcraft-gpu",
@@ -4390,7 +4392,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"image",
"moxcms",
@@ -4399,12 +4401,13 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcraft-geom",
"web-time",
"weezl",
]
[[package]]
name = "solidcraft-sketch"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"harfrust",
"serde",
@@ -4417,14 +4420,18 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-testkit"
version = "0.30.1"
[[package]]
name = "solidcraft-ui-egui"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"arboard",
"egui",
@@ -4443,6 +4450,7 @@ dependencies = [
"solidcraft-gpu",
"solidcraft-space-mouse",
"syn 2.0.119",
"sys-locale",
"tiny-skia",
"toml",
"ttf-parser",
@@ -4454,7 +4462,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"eframe",
"egui",
@@ -4558,6 +4566,18 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
dependencies = [
"js-sys",
"libc",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5362,9 +5382,9 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
dependencies = [
"rustls-pki-types",
]
@@ -5845,8 +5865,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winit"
version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
dependencies = [
"ahash",
"android-activity",
@@ -5999,7 +6017,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.26.0"
version = "0.30.1"
dependencies = [
"proc-macro2",
"serde",
+10 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.26.0"
version = "0.30.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -28,6 +28,8 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
solidcraft-mcp = { path = "crates/mcp" }
solidcraft-derive = { path = "crates/derive" }
solidcraft-3dx-client = { path = "crates/threedx-client" }
# test tooling: a dev-dependency only (xtask/src/layers.rs)
solidcraft-testkit = { path = "crates/testkit" }
# foundation
thiserror = "2"
@@ -161,6 +163,13 @@ matext4cgmath = { path = "vendor/matext4cgmath" }
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
egui-wgpu = { path = "vendor/egui-wgpu" }
# Deliver trackpad pinch and rotation gestures on Wayland (zwp_pointer_gestures_v1), which
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
winit = { path = "vendor/winit" }
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
accesskit_macos = { path = "vendor/accesskit_macos" }
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
# builds, since every mutant rebuilds the crates above the one it changes.
[profile.mutants]
+4 -3
View File
@@ -42,8 +42,8 @@
</p>
<p align="center">
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily" alt="tangency-labs%2Fsolidcraft | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily" alt="tangency-labs%2Fsolidcraft | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
</p>
![SolidCraft: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo.gif)
@@ -98,7 +98,7 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
runs its own design session, or with `--connect` it drives the running app so you can watch.
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
`"C:\\Program Files\\SolidCraft\\solidcraft-cli.exe"`). Tools, access rules and an example session:
[docs/mcp.md](docs/mcp.md).
## Scripts
@@ -150,3 +150,4 @@ Tinkercad are trademarks of their respective owners, named only to describe comp
[linux-appimage]: https://github.com/tangency-labs/solidcraft/releases/latest/download/solidcraft-linux-x86_64.AppImage
[snap-store]: https://snapcraft.io/solidcraft
[web-app]: https://solidcraft.io/app
+3 -1
View File
@@ -662,7 +662,9 @@ completed drawings publish atomically. The browser still exports synchronously.
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
sampling.
sampling. A fold crosses the sheet's own outer edges the same way, slanted or on any curve,
with the outline exact in the flat pattern and its DXF, and flanges and other folds still go
on the panels.
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
exact holes and retained islands, including the union of multiple selected profiles, and
+40 -12
View File
@@ -3,7 +3,7 @@
## Supported versions
SolidCraft is young and moves quickly, so only the latest release gets security fixes. Please
update to the [latest release](https://github.com/bherbruck/solidcraft/releases/latest) before
update to the [latest release](https://github.com/tangency-labs/solidcraft/releases/latest) before
reporting, and check whether the problem is still on `main`.
| Version | Supported |
@@ -15,18 +15,46 @@ reporting, and check whether the problem is still on `main`.
## Reporting a vulnerability
Please **don't open a public issue** for a security problem. Report it privately through GitHub:
open the repository's [Security tab](https://github.com/bherbruck/solidcraft/security) and choose
open the repository's [Security tab](https://github.com/tangency-labs/solidcraft/security) and choose
**Report a vulnerability**
([direct link](https://github.com/bherbruck/solidcraft/security/advisories/new)).
([direct link](https://github.com/tangency-labs/solidcraft/security/advisories/new)).
Please include what you found, how to reproduce it (a design file, command or MCP request), the
version and platform, and the impact you expect. I'll acknowledge the report as soon as I can,
keep you updated in the advisory, and credit you when the fix is published unless you'd rather
stay anonymous.
Please include what you found, the steps you ran to reproduce it (a design file, command or MCP
request), the version and platform, and what an attacker gains. I'll acknowledge the report as soon
as I can, keep you updated in the advisory, and credit you when the fix is published unless you'd
rather stay anonymous.
Areas where reports are especially useful:
## What counts as a security issue
- opening untrusted design, STEP, STL or 3MF files (crashes, excessive memory or CPU, path tricks)
- the MCP server and the control API (file access outside the allowed folders, command injection)
- the web app (anything reaching beyond the page's own storage)
- release packages and the update paths (AppImage, Flatpak, MSI, deb, rpm, snap)
Advisories are for problems where someone gets something they shouldn't, without already having
access to your computer or your account. For example:
- opening a design, STEP, STL or 3MF file from someone else runs code, or reads or overwrites files
it shouldn't
- a website or another computer can reach or control SolidCraft
- an agent gets past a prompt that promised to ask you first
- the web app reaches beyond the page's own storage
- a release package or an update can be tampered with
- an API key or token gets sent somewhere it shouldn't go
## What to report as a normal bug
Please [open an issue](https://github.com/tangency-labs/solidcraft/issues/new) for these. They're
still worth fixing, but they aren't security issues:
- anything that needs a program already running on your computer, or the control token. A program
running as you can already do what you can.
- MCP's allowed folders. They guard against accidents, they aren't a security boundary, and an agent
with a shell of its own can already reach those files. Getting around them isn't a vulnerability.
- crashes, hangs, or heavy memory or CPU use, unless they also lead to something in the list above
- anything that only happens in an unusual setup you'd have to choose on purpose
## Before you send it
- **Run it.** Reproduce the problem on the latest release. If you only read the code, say so, and
expect it to wait.
- **Check it yourself.** If a scanner or an AI agent found it, read the code and confirm it first.
These tools tend to overrate what they find.
- **Say what's gained.** Name what the attacker needs and what they get. I set the final severity.
Reports that don't fit will be closed with a short note, or moved to the issue tracker.
+47 -12
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 561 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.
@@ -353,7 +353,7 @@ rustls-native-certs 0.8.5 (Apache-2.0 OR ISC OR MIT)
rustls-pki-types 1.15.1 (MIT OR Apache-2.0)
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)
rustls-webpki 0.103.16 (ISC)
rustversion 1.0.23 (MIT OR Apache-2.0)
ruzstd 0.9.1 (MIT)
schannel 0.1.29 (MIT)
@@ -403,6 +403,7 @@ syn 1.0.109 (MIT OR Apache-2.0)
syn 2.0.119 (MIT OR Apache-2.0)
syn 3.0.6 (MIT OR Apache-2.0)
synstructure 0.13.2 (MIT)
sys-locale 0.3.2 (MIT OR Apache-2.0)
tempfile 3.27.0 (MIT OR Apache-2.0)
termcolor 1.4.1 (Unlicense OR MIT)
thiserror 1.0.69 (MIT OR Apache-2.0)
@@ -435,7 +436,7 @@ 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)
typed-path 0.13.0 (MIT OR Apache-2.0)
typeid 1.0.3 (MIT OR Apache-2.0)
typenum 1.20.1 (MIT OR Apache-2.0)
uds_windows 1.2.1 (MIT)
@@ -474,7 +475,7 @@ wayland-sys 0.31.11 (MIT)
web-sys 0.3.106 (MIT OR Apache-2.0)
web-time 1.1.0 (MIT OR Apache-2.0)
webbrowser 1.2.4 (MIT OR Apache-2.0)
webpki-root-certs 1.0.9 (CDLA-Permissive-2.0)
webpki-root-certs 1.0.10 (CDLA-Permissive-2.0)
weezl 0.1.12 (MIT OR Apache-2.0)
wgpu 30.0.1 (MIT OR Apache-2.0)
wgpu-core 30.0.1 (MIT OR Apache-2.0)
@@ -552,7 +553,7 @@ 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 9.0.4 (MIT)
zip 9.1.3 (MIT)
zlib-rs 0.6.8 (Zlib)
zmij 1.0.23 (MIT)
zopfli 0.8.3 (Apache-2.0)
@@ -596,7 +597,7 @@ pin-project 1.1.13, pin-project-internal 1.1.13, pin-project-lite 0.2.17, piper
rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, serde 1.0.229, serde_core 1.0.229,
serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json 1.0.151, serde_path_to_error 0.1.20,
serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 1.0.109, syn 2.0.119, syn 3.0.6, thiserror
1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.12.3, typeid
1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.13.0, typeid
1.0.3, unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2
1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, xdg-home 1.3.0, zbus-lockstep 0.5.2,
zbus-lockstep-macros 0.5.2, zbus_names 3.0.0, zmij 1.0.23, zvariant_utils 2.1.0, zvariant_utils
@@ -1339,7 +1340,7 @@ libc 0.2.190, litrs 1.0.0, num_enum 0.7.6, num_enum_derive 0.7.6, proc-macro2 1.
ref-cast 1.0.27, ref-cast-impl 1.0.27, rustc-hash 2.1.3, rustversion 1.0.23, semver 1.0.28, serde
1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json
1.0.151, serde_path_to_error 0.1.20, serde_repr 0.1.21, syn 2.0.119, syn 3.0.6, thiserror 1.0.69,
thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.12.3, typeid 1.0.3,
thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.13.0, typeid 1.0.3,
unicode-ident 1.0.26, utf8-zero 0.8.1
================================================================================
@@ -1836,8 +1837,8 @@ approx 0.4.0, as-raw-xcb-connection 1.0.1, atspi 0.29.0, atspi-common 0.13.0, at
cgmath 0.18.0, chardetng 0.1.17, codespan-reporting 0.13.1, encoding_rs 0.8.42, kurbo 0.13.1,
multiversion_no_op 1.0.0, nohash-hasher 0.2.0, peniko 0.6.1, polycool 0.4.0, raw-window-metal 1.1.0,
rustls-platform-verifier 0.7.1, self_cell 1.3.0, siphasher 1.0.4, spade 2.15.1, static_assertions
1.1.0, ureq 3.4.2, ureq-proto 0.6.4, utf8_iter 1.0.4, x11rb 0.13.2, x11rb-protocol 0.13.2, zeroize
1.9.1, zeroize_derive 1.5.0, zune-core 0.5.3, zune-jpeg 0.5.15
1.1.0, sys-locale 0.3.2, ureq 3.4.2, ureq-proto 0.6.4, utf8_iter 1.0.4, x11rb 0.13.2, x11rb-protocol
0.13.2, zeroize 1.9.1, zeroize_derive 1.5.0, zune-core 0.5.3, zune-jpeg 0.5.15
================================================================================
Apache License
@@ -12431,7 +12432,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
rustls-webpki 0.103.15
rustls-webpki 0.103.16
================================================================================
Except as otherwise noted, this project is licensed under the following
@@ -13570,6 +13571,32 @@ The above copyright notice and this permission notice shall be included in all c
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.
================================================================================
sys-locale 0.3.2
================================================================================
MIT License
Copyright (c) 2021 1Password
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.
================================================================================
tempfile 3.27.0
================================================================================
@@ -15274,7 +15301,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
webpki-root-certs 1.0.9
webpki-root-certs 1.0.10
================================================================================
# Community Data License Agreement - Permissive - Version 2.0
@@ -16401,7 +16428,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
zip 9.0.4
zip 9.1.3
================================================================================
The MIT License (MIT)
@@ -16799,6 +16826,14 @@ Bundled fonts (SIL Open Font License, Version 1.1)
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)
Source: https://github.com/googlefonts/morisawa-biz-ud-gothic
- CraftUI-SC.otf (assets/fonts/craft-ui-sc/CraftUI-SC.otf)
Copyright 2014-2021 Adobe / Google Inc. (Noto Sans SC subset for SolidCraft web zh)
Source: https://github.com/notofonts/noto-cjk (Sans SubsetOTF/SC)
- CraftUI-TC.otf (assets/fonts/craft-ui-tc/CraftUI-TC.otf)
Copyright 2014-2021 Adobe / Google Inc. (Noto Sans TC subset for SolidCraft web zh-TW)
Source: https://github.com/notofonts/noto-cjk (Sans SubsetOTF/TC)
- IBMPlexSans-Variable.ttf (assets/fonts/ibm-plex-sans/IBMPlexSans-Variable.ttf)
Copyright 2017 IBM Corp. with Reserved Font Name "Plex"
Source: https://github.com/IBM/plex
@@ -66,7 +66,7 @@ fn run() -> Result<(), String> {
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 (commands, command_timings) = session.run_script_timed(&recipe, false, &mut |_, _| {}).map_err(|e| e.to_string())?;
let generation_seconds = generation_start.elapsed().as_secs_f64();
let design_path = output.with_extension(solidcraft_engine::io::DESIGN_EXT);
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
@@ -87,7 +87,7 @@ fn run() -> Result<(), String> {
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")?;
solidcraft_gpu::select::pick_path(policy, |_| true).ok_or("no Vulkan or Metal 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 {
+62 -37
View File
@@ -2,6 +2,7 @@
//!
//! ```text
//! solidcraft-cli run <script.json|design.sc|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
//! [--param-check off|warn|strict]
//! solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.sc> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
@@ -57,7 +58,7 @@ struct Command {
}
/// `run`'s lines in the usage.
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.sc|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";
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\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.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
/// `script`'s lines in the usage.
@@ -171,7 +172,11 @@ fn main() -> ExitCode {
Some("oracle") => oracle::run(&args[1..]),
Some("fuzz") => fuzz::run(&args[1..]),
Some("fuzz-one") => fuzz::one(&args[1..]),
Some("mcp") => cmd_mcp(&args[1..]),
Some("mcp") => {
let result = cmd_mcp(&args[1..]);
solidcraft_mcp::cleanup_scratch_dir();
result
}
Some("crashes") => crashes::run(&args[1..]),
Some("icon") => cmd_icon(&args[1..]),
Some("licences" | "licenses") => {
@@ -299,18 +304,36 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
} else {
let script = if v.get("features").is_some() && v.get("commands").is_none() { recipe::to_script(&v)? } else { v };
// Warn level: each step's `param_warnings` as it completes, even when a later step fails
// (stderr, so stdout stays the caller's).
let mut warn = |step: usize, r: &Value| {
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
}
};
s.run_script_each(&script, &mut warn).map_err(|e| e.to_string())?;
run_script_warning(&mut s, path, &script)?;
}
Ok(s)
}
/// Run `script`, read from `path`, on `s`. At the warn level each step's `param_warnings` go to
/// stderr as the step completes, even when a later step fails (stdout stays the caller's).
fn run_script_warning(s: &mut Session, path: &str, script: &Value) -> Result<Vec<Value>, String> {
s.run_script_each(script, &mut |step, r| print_param_warnings(path, step, r)).map_err(|e| e.to_string())
}
/// Step `step`'s `param_warnings` on stderr, one per line, named after the script at `path`.
fn print_param_warnings(path: &str, step: usize, r: &Value) {
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
}
}
/// `--param-check off|warn|strict`: how calls that break a command's parameter contract are
/// treated (docs/mcp.md); off when the option isn't given.
fn parse_param_check(cmd: &str, args: &[String]) -> Result<solidcraft_engine::schema::ParamCheck, String> {
match flag(args, "--param-check") {
None if args.iter().any(|a| a == "--param-check") => Err(format!("{cmd}: --param-check wants off, warn or strict")),
None => Ok(solidcraft_engine::schema::ParamCheck::Off),
Some(v) => {
solidcraft_engine::schema::ParamCheck::parse(v).ok_or_else(|| format!("{cmd}: --param-check wants off, warn or strict, not `{v}`"))
}
}
}
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).map(String::as_str)
}
@@ -367,7 +390,15 @@ fn print_json(v: &Value, file: Option<&str>) -> Result<(), String> {
fn cmd_run(args: &[String]) -> Result<(), String> {
install_renderer();
let json_out = json_out(args)?;
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
// Before the option check, so a bare `--param-check` gets the words `mcp` uses.
let check = parse_param_check("run", args)?;
known_args(
"run",
args,
&["--in", "--out", "--save", "--json-out", "--param-check"],
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
true,
)?;
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
@@ -380,23 +411,25 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
let (mut s, results) = if want_results || timings {
// Collect every command's result or time (scripts only), then process requested files too.
let mut s = match base {
Some(b) => load(b)?,
Some(b) => load_checked(b, check)?,
None => Session::default(),
};
s.param_check = check;
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
let v = read_json(path.ok_or(needs)?)?;
let script = path.ok_or(needs)?;
let v = read_json(script)?;
let out = if timings {
let (r, mut t) = s.run_script_timed(&v, profile).map_err(|e| e.to_string())?;
let (r, mut t) = s.run_script_timed(&v, profile, &mut |step, r| print_param_warnings(script, step, r)).map_err(|e| e.to_string())?;
if want_results && let Some(o) = t.as_object_mut() {
o.insert("results".into(), Value::Array(r));
}
t
} else {
Value::Array(s.run_script(&v).map_err(|e| e.to_string())?)
Value::Array(run_script_warning(&mut s, script, &v)?)
};
(s, Some(out))
} else {
(open(base, path)?, None)
(open(base, path, check)?, None)
};
// A feature that fails leaves the bodies as they were before it (a parameter change can
// break a feature without failing the command that made it): writing them would hand out
@@ -421,16 +454,16 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
}
/// The session `run`, `eval` and `snapshot` start from: `--in` (a design or STEP file) with the
/// positional script run on top of it, or either one alone.
fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
/// positional script run on top of it, or either one alone, with scripts run at `check`.
fn open(base: Option<&str>, path: Option<&String>, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
match (base, path) {
(Some(b), Some(p)) => {
let mut s = load(b)?;
s.run_script(&read_json(p)?).map_err(|e| e.to_string())?;
let mut s = load_checked(b, check)?;
run_script_warning(&mut s, p, &read_json(p)?)?;
Ok(s)
}
(Some(b), None) => load(b),
(None, Some(p)) => load(p),
(Some(b), None) => load_checked(b, check),
(None, Some(p)) => load_checked(p, check),
(None, None) => Err(usage()),
}
}
@@ -473,7 +506,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
let mut s = open(flag(args, "--in"), positional(args))?;
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
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())?;
let out = json!({
@@ -567,7 +600,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
Some(x) => x.parse().ok().filter(|n| (16..=8192).contains(n)).ok_or_else(|| format!("{name} must be 16 to 8192 pixels, not `{x}`")),
};
let (w, h) = (size("--width", 1280)?, size("--height", 800)?);
let mut s = open(flag(args, "--in"), positional(args))?;
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
// How the bodies are drawn and where the model is cut, through the commands the app's
// Appearance dialog and Section Analysis run (`preview`).
preview::apply(&mut s, &flags(args, "--body"), &flags(args, "--tool"), flag(args, "--section"))?;
@@ -691,17 +724,8 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
}
}
// How a headless session treats calls that break a command's parameter contract (docs/mcp.md).
let param_check = match flag(args, "--param-check") {
None if args.iter().any(|a| a == "--param-check") => return Err("mcp: --param-check wants off, warn or strict".into()),
None => solidcraft_engine::schema::ParamCheck::Off,
Some(v) => {
solidcraft_engine::schema::ParamCheck::parse(v).ok_or_else(|| format!("mcp: --param-check wants off, warn or strict, not `{v}`"))?
}
};
if param_check != solidcraft_engine::schema::ParamCheck::Off && flag(args, "--connect").is_some() {
eprintln!("solidcraft mcp: --param-check applies to headless sessions; calls to the app are not checked yet");
}
// How calls that break a command's parameter contract are treated, headless or bridged.
let param_check = parse_param_check("mcp", args)?;
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
(Some(addr), None) => {
@@ -709,7 +733,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
return Err(format!("mcp: --connect wants HOST:PORT, such as 127.0.0.1:7878, not `{addr}`"));
};
// Serve without the app too: the tools connect once it runs, and say it isn't until then.
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token"));
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token")).with_param_check(param_check);
if let Err(e) = remote.try_connect() {
eprintln!(
"solidcraft mcp: no SolidCraft app at {addr} yet ({e}); start it with `solidcraft --control {port}` \
@@ -729,7 +753,8 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
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 remote = solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token")).with_param_check(param_check);
return serve_mcp(Box::new(remote), scope);
}
let mut headless = solidcraft_mcp::Headless::default();
headless.session.param_check = param_check;
+107
View File
@@ -263,3 +263,110 @@ mod setup {
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
}
}
/// Scratch files belong to the CLI process; exercise real startup, requests and shutdown.
mod scratch_cleanup {
use super::*;
use std::path::{Path, PathBuf};
fn command(case: &str) -> (PathBuf, Command) {
let dir = std::env::temp_dir().join(format!("solidcraft-mcp-lifecycle-{case}-{}", std::process::id()));
std::fs::create_dir(&dir).unwrap();
let mut command = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
command
.args(["mcp", "--headless"])
.env("TMPDIR", &dir)
.env("TMP", &dir)
.env("TEMP", &dir)
.env("SOLIDCRAFT_RECOVERY_DIR", dir.join("recovery"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
(dir, command)
}
fn scratch_folders(dir: &Path) -> Vec<PathBuf> {
std::fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.file_name().unwrap().to_string_lossy().starts_with("solidcraft-mcp-"))
.collect()
}
fn initialize() -> Value {
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}})
}
fn run(mut command: Command, request: Value) -> std::process::Output {
let mut child = command.spawn().unwrap();
writeln!(child.stdin.take().unwrap(), "{request}").unwrap();
child.wait_with_output().unwrap()
}
#[test]
fn eof_removes_only_the_process_scratch_folder() {
let (dir, command) = command("eof");
let unrelated = dir.join("solidcraft-mcp-unrelated");
std::fs::create_dir(&unrelated).unwrap();
let out = run(command, initialize());
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
assert!(reply.get("result").is_some(), "{reply}");
assert_eq!(scratch_folders(&dir), vec![unrelated]);
assert!(dir.join("recovery").is_dir(), "cleanup must leave the recovery folder alone");
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn startup_error_removes_the_process_scratch_folder() {
let (dir, mut command) = command("startup-error");
let out = command.args(["--param-check", "invalid"]).stdin(Stdio::null()).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(stderr.contains("--param-check wants off, warn or strict"), "{stderr}");
assert!(scratch_folders(&dir).is_empty(), "startup error left a scratch folder in {dir:?}");
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn shutdown_preserves_nonempty_scratch_folder() {
let (dir, mut command) = command("nonempty");
let mut child = command.spawn().unwrap();
let mut input = child.stdin.take().unwrap();
writeln!(input, "{}", initialize()).unwrap();
let mut reply = String::new();
BufReader::new(child.stdout.take().unwrap()).read_line(&mut reply).unwrap();
let reply: Value = serde_json::from_str(&reply).unwrap();
assert!(reply.get("result").is_some(), "{reply}");
let scratch = scratch_folders(&dir);
assert_eq!(scratch.len(), 1);
let saved = scratch[0].join("keep.txt");
std::fs::write(&saved, "saved output").unwrap();
drop(input);
let out = child.wait_with_output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
assert_eq!(std::fs::read_to_string(saved).unwrap(), "saved output");
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn failed_screenshot_scratch_folder_is_removed_without_a_path_scope() {
let (dir, mut command) = command("screenshot");
command.arg("--allow-any-path");
// A headless server cannot capture a window. The request still allocates a scratch
// path before the backend refuses it, without relying on a GPU or installed renderer.
let out = run(
command,
json!({
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "screenshot", "arguments": {"source": "window"}},
}),
);
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(reply["result"]["isError"], true, "{reply}");
assert!(reply["result"]["content"][0]["text"].as_str().unwrap().contains("the window gave no picture"), "{reply}");
assert!(scratch_folders(&dir).is_empty(), "failed screenshot left a scratch folder in {dir:?}");
std::fs::remove_dir_all(dir).unwrap();
}
}
+44
View File
@@ -288,3 +288,47 @@ fn undoing_an_edit_after_opening_reuses_the_saved_results() {
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A design saved before body names were pinned (#6403) pins them as it opens, as its first
/// command would, and opening it is no change. That command pinned them as part of itself, so
/// undoing it went back to the unpinned design, which no evaluated result matched: the first
/// undo after opening from the command line evaluated every feature again (82-93 s in a
/// 5,484-feature design); `doc.open` pinned them after opening, which marked the design changed.
#[test]
fn a_design_saved_before_names_were_pinned_opens_pinned_and_unchanged() {
let plate = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Sample Plate").unwrap();
let mut built = Session::default();
for c in (plate.script)()["commands"].as_array().unwrap() {
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
let mut doc = built.design().into_owned();
for f in &mut doc.features {
f.body_names.clear();
}
doc.reserved_body_names.clear();
let unpinned = Session::new(doc);
let design = unpinned.design();
let saved = unpinned.model.saved(&design).unwrap();
let bytes = solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(&saved))
.unwrap();
let path = temp("unpinned");
std::fs::write(&path, &bytes).unwrap();
let file = path.to_string_lossy().to_string();
// As the command line opens a design (`load`), and as the app does (`doc.open`).
let (doc, saved) = solidcraft_engine::io::read_design_file(&file, None::<fn(solidcraft_engine::io::DesignPreview)>).unwrap();
for (how, mut s) in [("opened_at", Session::opened_at(doc, saved.as_ref(), Some(&file))), ("doc.open", open(&path))] {
assert_eq!(s.model.last_recomputed, 0, "{how}: taken from the file");
assert!(s.doc.features.iter().any(|f| !f.body_names.is_empty()), "{how}: the names are pinned");
assert!(!s.is_dirty(), "{how}: opening is no change");
let read: Vec<Arc<ModelState>> = s.model.results.iter().map(|r| r.state.clone()).collect();
s.execute("solid.box", &json!({"length": 3, "width": 3, "height": 3, "operation": "new"})).unwrap();
// As in a large design, whose evaluation cache keeps only a few results: undo finds the
// results in the model itself or evaluates them again.
*s.model.cache.lock().unwrap() = Default::default();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.model.last_recomputed, 0, "{how}: the first undo evaluates nothing");
assert!(s.model.results.iter().zip(&read).all(|(r, st)| Arc::ptr_eq(&r.state, st)), "{how}: the states read from the file");
assert!(!s.is_dirty(), "{how}: undone to the design as opened");
}
let _ = std::fs::remove_file(&path);
}
+75 -1
View File
@@ -229,7 +229,9 @@ fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
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);
// #4907: `run --save` maps to Save As, and a borrowed design name
// follows the destination file's stem instead of keeping the old name.
assert_eq!(reopened.name, format!("resaved-{index}"));
assert_eq!(reopened.features, document.features);
assert_eq!(std::fs::read(&source).unwrap(), bytes);
}
@@ -496,6 +498,78 @@ fn timings_and_profile_report_each_command() {
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
}
/// #6921: `--param-check` checks a script's steps against their contracts on every path `run`
/// takes (a script alone, on an `--in` design or script, with `--results`, `--timings` or
/// `--profile`), and a warning reaches stderr even when a later step fails.
#[test]
fn param_check_reaches_every_script_run_takes() {
let dir = Scratch::new();
let typo = dir.0.join("typo.json");
let step = json!({"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4, "operaton": "new"}});
std::fs::write(&typo, json!({"commands": [step.clone()]}).to_string()).unwrap();
let base = dir.0.join("base.solidcraft");
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&base).output().unwrap());
let saved = dir.0.join("saved.solidcraft");
let start = dir.script();
let (typo, base, saved_arg, start) = (typo.to_str().unwrap(), base.to_str().unwrap(), saved.to_str().unwrap(), start.to_str().unwrap());
let paths = [&[][..], &["--in", base], &["--in", start], &["--results"], &["--timings"], &["--timings", "--profile"]];
let run = |level: Option<&str>, extra: &[&str]| {
std::fs::remove_file(&saved).ok();
let check = level.map(|l| ["--param-check", l]).into_iter().flatten();
cli().args(["run", typo, "--save", saved_arg]).args(check).args(extra).output().unwrap()
};
// Off, the default: the step runs as it always has, without a word.
let off = run(None, &[]);
assert!(off.status.success() && !String::from_utf8_lossy(&off.stderr).contains("operaton"), "{off:?}");
assert_eq!(body_count(&saved), 1);
// Warn: each path runs the step, saves, and reports it on stderr; `--results` keeps it too.
for extra in paths {
let output = run(Some("warn"), extra);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{extra:?}: {stderr}");
assert!(stderr.contains("step 1: unknown key `operaton`; did you mean `operation`?"), "{extra:?}: {stderr}");
assert!(saved.exists(), "{extra:?}");
if extra == ["--results"] {
let results: Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(results[0]["param_warnings"][0].as_str().is_some_and(|w| w.contains("operaton")), "{results}");
}
}
// Strict: on each path the run stops at the step, and nothing is written.
for extra in paths {
let output = run(Some("strict"), extra);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "{extra:?}: {stderr}");
assert!(stderr.contains("unknown key `operaton`"), "{extra:?}: {stderr}");
assert!(!saved.exists(), "{extra:?}");
}
// Warn, with a later step that fails: the run fails, and the first step's warning is still
// reported, timed or not.
let failing = dir.0.join("failing.json");
let fail = json!({"command": "solid.extrude", "params": {"sketch": "Nope"}});
std::fs::write(&failing, json!({"commands": [step, fail]}).to_string()).unwrap();
for extra in [&[][..], &["--timings"]] {
let output = cli().arg("run").arg(&failing).args(["--param-check", "warn"]).args(extra).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "{extra:?}: {stderr}");
assert!(stderr.contains("step 1: unknown key `operaton`") && stderr.contains("no sketch `Nope`"), "{extra:?}: {stderr}");
}
// A level it doesn't know and a missing value fail.
for (bad, says) in [
(&["--param-check", "loud"][..], "--param-check wants off, warn or strict, not `loud`"),
(&["--param-check"], "run: --param-check wants off, warn or strict"),
] {
let output = cli().args(["run", typo]).args(bad).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "{bad:?}: {stderr}");
assert!(stderr.contains(says), "{bad:?}: {stderr}");
}
}
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
let path = dir.0.join("render.json");
std::fs::write(
@@ -123,3 +123,42 @@ fn a_design_naming_a_sketch_and_its_extrude_alike_replays_from_its_script() {
assert_eq!(names, ["Block", "Block"], "{first:#}");
assert_eq!(second["commands"], first["commands"], "the replayed design's script has the same steps");
}
/// #10230: script export must disclose the print-face state that replay leaves out.
#[test]
fn script_export_warns_about_omitted_print_faces_on_stderr() {
let dir = std::env::temp_dir().join(format!("solidcraft-print-face-script-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let saved = dir.join("block.sc");
let exported = dir.join("block.json");
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 20, "width": 10, "height": 5, "body_name": "Block"})).unwrap();
s.execute("body.print_face", &json!({"body": "Block", "point": [10, 5, 0]})).unwrap();
s.execute("file.save", &json!({"path": saved})).unwrap();
let native = std::fs::read(&saved).unwrap();
for to_file in [false, true] {
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
cmd.arg("script").arg(&saved);
if to_file {
cmd.arg("--out").arg(&exported);
}
let out = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr}");
assert!(stderr.contains("note: the script leaves out print-face assignments (body.print_face)"), "{stderr}");
let script = if to_file {
assert!(out.stdout.is_empty(), "--out must keep stdout empty");
std::fs::read(&exported).unwrap()
} else {
out.stdout
};
let script: Value = serde_json::from_slice(&script).unwrap();
let mut replayed = Session::default();
replayed.run_script(&script).unwrap();
assert!(replayed.doc.print_faces.is_empty());
assert_eq!(replayed.doc.features, s.doc.features);
assert_eq!(std::fs::read(&saved).unwrap(), native, "export must not change the source design");
}
std::fs::remove_dir_all(dir).unwrap();
}
+1 -1
View File
@@ -34,7 +34,7 @@ serde_json = { workspace = true }
web-sys = { version = "0.3", features = [
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage", "StorageEvent",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "FormData", "RequestInit", "MediaQueryList", "Gamepad", "Performance",
+4
View File
@@ -22,6 +22,10 @@
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/CraftUI-SC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
+27 -8
View File
@@ -15,6 +15,9 @@ type Reply = Result<(String, Vec<u8>), String>;
pub struct Download {
reply: Rc<RefCell<Option<Reply>>>,
pending: Option<(String, Vec<u8>)>,
/// The downloaded design being opened (a few features a frame, #10198): the answer to come,
/// the name it takes and the file it was read from.
opening: Option<(std::sync::mpsc::Receiver<serde_json::Value>, String, String)>,
}
impl Download {
@@ -33,13 +36,14 @@ impl Download {
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
self.opened(app);
if let Some(reply) = self.reply.borrow_mut().take() {
match reply {
Ok(file) => self.pending = Some(file),
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
}
}
if self.pending.is_none() || app.file_picker_busy() {
if self.pending.is_none() || self.opening.is_some() || app.file_picker_busy() {
return;
}
if solidcraft_ui_egui::documents::any_dirty(app) {
@@ -71,18 +75,33 @@ impl Download {
}
}
let Some((name, bytes)) = self.pending.take() else { return };
// The extension selects the design loader even when the URL has no extension.
let path = &format!("/linked/design.{}", solidcraft_engine::io::DESIGN_EXT);
vfs::insert(path, bytes);
app.open_path(path);
if app.session.path.as_deref() == Some(path.as_str()) {
// The extension selects the design loader even when the URL has no extension. Named
// after the link, as the progress card shows the file's name.
let file: String = name.chars().map(|c| if matches!(c, '/' | '\\') { '_' } else { c }).collect();
let path = format!("/linked/{}.{}", if file.is_empty() { "design" } else { &file }, solidcraft_engine::io::DESIGN_EXT);
vfs::insert(&path, bytes);
// Opened as any design is, with its progress and Cancel; finished in `opened`.
self.opening = Some((app.open_path_answered(&path), name, path));
self.opened(app);
}
/// The downloaded design is in: it is an unsaved copy under the link's name.
fn opened(&mut self, app: &mut SolveApp) {
let Some((rx, _, _)) = &self.opening else { return };
let (ok, cancelled) = match rx.try_recv() {
Ok(reply) => (reply["ok"].as_bool() == Some(true), reply["error"].as_str().is_some_and(|e| e.contains("cancelled"))),
Err(std::sync::mpsc::TryRecvError::Empty) => return,
Err(std::sync::mpsc::TryRecvError::Disconnected) => (false, false),
};
let Some((_, name, path)) = self.opening.take() else { return };
if ok && app.session.path.as_deref() == Some(path.as_str()) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
} else {
} else if !ok && !cancelled {
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
}
let _ = vfs::remove(path);
let _ = vfs::remove(&path);
}
}
+211 -36
View File
@@ -119,6 +119,43 @@ fn closes_design(key: &str, command: bool, shift: bool, alt: bool, design_shown:
design_shown && command && !shift && !alt && key.eq_ignore_ascii_case("w")
}
/// The settings a tab last wrote to the browser's storage, or loaded from another tab (#8644).
/// Every open tab of the app shares that storage, and eframe saves each tab's whole copy every
/// 30 s and when the page loses focus, so a tab still holding old settings put them back over a
/// change made in another. A change is now written in the frame it is made, and the other tabs
/// load it ([`settings_written`]).
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Default, Debug)]
struct PrefsWriter {
written: Option<String>,
}
#[cfg(any(target_arch = "wasm32", test))]
impl PrefsWriter {
/// `prefs` if they are not the settings written or loaded last (they count as written from
/// now on).
fn take(&mut self, prefs: String) -> Option<String> {
if self.written.as_deref() == Some(prefs.as_str()) {
return None;
}
self.written = Some(prefs.clone());
Some(prefs)
}
/// The settings the tab holds after loading another tab's: the tab doesn't write them back.
fn loaded(&mut self, prefs: String) {
self.written = Some(prefs);
}
}
/// The settings another tab wrote, from a `storage` event (the browser sends one to every other
/// tab of the page when one tab writes). Other keys, and settings taken away (clearing the site's
/// data), leave this tab's settings as they are.
#[cfg(any(target_arch = "wasm32", test))]
fn settings_written(key: Option<&str>, value: Option<String>) -> Option<String> {
value.filter(|_| key == Some(solidcraft_ui_egui::prefs::STORAGE_KEY))
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
@@ -217,9 +254,29 @@ mod web {
on_key.forget();
}
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
/// Settings another tab wrote, waiting for this tab's next frame (#8644).
type PrefsInbox = Rc<std::cell::Cell<Option<String>>>;
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
/// Settings another tab of the app writes go to `inbox`, and this tab draws a frame, which
/// loads them.
fn hear_settings_from_other_tabs(window: &web_sys::Window, ctx: &egui::Context, inbox: PrefsInbox) {
let ctx = ctx.clone();
let on_storage = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::StorageEvent)>::new(move |e: web_sys::StorageEvent| {
if let Some(prefs) = crate::settings_written(e.key().as_deref(), e.new_value()) {
inbox.set(Some(prefs));
ctx.request_repaint();
}
});
let _ = window.add_event_listener_with_callback("storage", on_storage.as_ref().unchecked_ref());
// The listener lives as long as the page.
on_storage.forget();
}
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
const CHINESE_SC_FONT_PATH: &str = "./fonts/CraftUI-SC.otf";
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
struct Shell {
app: SolveApp,
@@ -237,18 +294,63 @@ mod web {
rename_focus: bool,
/// The start page's recent designs were filled from the stored ones.
recents_synced: bool,
/// On-demand CJK font: fetched only when a CJK language is selected.
/// On-demand script fonts: fetched only when a CJK language is selected.
font: solidcraft_ui_egui::fonts::CjkFontLoader,
font_result: FontLoadResult,
/// The fetched CJK font's bytes: kept so a language change can re-order the fallbacks.
/// The fetched Japanese font's bytes: kept so a language change can re-order the fallbacks.
cjk_bytes: Option<Vec<u8>>,
/// The fetched Chinese font's bytes (Simplified or Traditional for this session).
chinese_bytes: Option<Vec<u8>>,
/// Which Chinese face `chinese_bytes` is: `"zh"` or `"zh-TW"`.
chinese_kind: Option<&'static str>,
/// Kind requested by the in-flight fetch (`"ja"`, `"zh"`, or `"zh-TW"`).
font_fetch_kind: Option<&'static str>,
/// 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,
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
prefs_writer: crate::PrefsWriter,
other_tab_prefs: PrefsInbox,
}
impl Shell {
/// The settings as they are kept between visits: with the user's own language, not a
/// session-only English fallback.
fn persisted_prefs(&mut self) -> String {
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
let prefs = self.app.prefs();
self.app.preferences.language = live;
prefs
}
/// A settings change goes to the browser's storage in the frame it is made, and from
/// there to the other open tabs (#8644). Input is what changes settings, so a frame
/// without any is skipped; eframe's own save still catches anything else.
fn store_prefs(&mut self, ctx: &egui::Context) {
if !self.launch.persists() || ctx.input(|i| i.events.is_empty()) {
return;
}
let prefs = self.persisted_prefs();
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
let stored =
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(solidcraft_ui_egui::prefs::STORAGE_KEY, &prefs));
if !matches!(stored, Some(Ok(()))) {
log::warn!("settings: the browser's storage did not take them; they are saved again later");
self.prefs_writer = crate::PrefsWriter::default();
}
}
/// Settings another tab wrote replace this tab's, so this tab's own saves keep them
/// instead of putting its older copy back (#8644).
fn load_other_tabs_prefs(&mut self) {
let Some(prefs) = self.other_tab_prefs.take() else { return };
self.app.load_prefs(&prefs);
let held = self.persisted_prefs();
self.prefs_writer.loaded(held);
}
/// Files from the computer, writes to store, messages and the autosave.
fn files_tick(&mut self, ctx: &egui::Context) {
if !self.launch.persists() {
@@ -273,7 +375,8 @@ mod web {
// Designs opened from the computer's disk (the files window): open them.
let opened: Vec<_> = self.files.opened.borrow_mut().drain(..).collect();
for path in opened {
self.app.open_path(&path);
// A few features a frame, with progress and Cancel (#10198).
self.app.open_path_async(&path);
self.files.panel.set(false);
}
// Recent designs whose file is gone are dropped from the list.
@@ -314,7 +417,7 @@ mod web {
path
};
match intent {
Intent::Open => self.app.open_path(&path),
Intent::Open => self.app.open_path_async(&path),
Intent::Insert | Intent::Theme => self.app.drop_path(&path),
}
self.files.panel.set(false);
@@ -478,7 +581,7 @@ mod web {
let path = format!("{}{name}", files::OPFS);
match what {
"open" => {
self.app.open_path(&path);
self.app.open_path_async(&path);
self.files.panel.set(false);
}
"insert" => {
@@ -580,65 +683,102 @@ mod web {
files::autosave(&self.files, None);
}
/// Fetch and install the CJK font when a CJK language is selected. If that fails, this
/// Whether the shell already has the on-demand face the live language needs.
fn script_font_ready(&self) -> bool {
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
Some("zh") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh"),
Some("zh-TW") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh-TW"),
Some("ja") => self.cjk_bytes.is_some(),
_ => true,
}
}
/// Fetch and install a script font when a CJK language is selected. If that fails, this
/// session falls back to English (no tofu); the saved language preference is kept.
fn tick_font(&mut self, ctx: &egui::Context) {
let done = self.font_result.borrow_mut().take();
if let Some(res) = done {
let kind = self.font_fetch_kind.take();
let installed = match res {
Ok(bytes) => {
self.cjk_bytes = Some(bytes);
Ok((path, bytes)) => {
match kind {
Some("zh") => {
self.chinese_bytes = Some(bytes);
self.chinese_kind = Some("zh");
}
Some("zh-TW") => {
self.chinese_bytes = Some(bytes);
self.chinese_kind = Some("zh-TW");
}
_ => {
self.cjk_bytes = Some(bytes);
}
}
log::info!("loaded web script font from {path}");
true
}
Err(err) => {
log::warn!("failed to fetch CJK font from {JAPANESE_FONT_PATH}: {err}");
let path = match kind {
Some("zh") => CHINESE_SC_FONT_PATH,
Some("zh-TW") => CHINESE_TC_FONT_PATH,
_ => JAPANESE_FONT_PATH,
};
log::warn!("failed to fetch script font from {path}: {err}");
false
}
};
if installed {
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
log::warn!("CJK font unavailable; showing English for this session");
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(effective) {
log::warn!("script font unavailable; showing English for this session");
}
self.font.finished(installed, &mut self.app.preferences.language);
self.apply_fonts(ctx);
ctx.request_repaint();
}
if self.font.tick(&mut self.app.preferences.language) {
self.fetch_font(ctx);
let ready = self.script_font_ready();
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
if self.font.tick(&mut self.app.preferences.language, ready) {
self.fetch_font(ctx, solidcraft_ui_egui::fonts::web_script_font_kind(effective).unwrap_or("ja"));
}
// The CJK face leads only a CJK UI: its Cyrillic glyphs are full-width, so switching
// to Ukrainian (or English) must put the Latin face back in front.
if self.app.preferences.language != self.font_language {
if solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language) != self.font_language {
self.apply_fonts(ctx);
}
}
/// Set up the UI fonts for the live language, keeping the fetched CJK font as a fallback
/// (it leads only for a CJK UI, see [`solidcraft_ui_egui::fonts::install_cjk_font_for`]).
/// Set up the UI fonts for the live language, keeping fetched script fonts as fallbacks
/// (Chinese leads a Chinese UI; see [`solidcraft_ui_egui::fonts::install_web_script_fonts`]).
fn apply_fonts(&mut self, ctx: &egui::Context) {
match self.cjk_bytes.clone() {
Some(bytes) => {
solidcraft_ui_egui::fonts::install_cjk_font_for(ctx, bytes, &self.app.preferences.language);
}
None => solidcraft_ui_egui::fonts::setup_fonts(ctx),
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
if self.cjk_bytes.is_none() && self.chinese_bytes.is_none() {
solidcraft_ui_egui::fonts::setup_fonts(ctx);
} else {
solidcraft_ui_egui::fonts::install_web_script_fonts(ctx, self.cjk_bytes.clone(), self.chinese_bytes.clone(), effective);
}
self.font_language.clone_from(&self.app.preferences.language);
self.font_language = effective.to_owned();
}
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
/// fetch the font, so the text does not stay tofu (#720).
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
/// fetch the Japanese font, so the text does not stay tofu (#720).
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
self.fetch_font(ctx);
self.fetch_font(ctx, "ja");
}
}
fn fetch_font(&self, ctx: &egui::Context) {
fn fetch_font(&mut self, ctx: &egui::Context, kind: &'static str) {
let path = match kind {
"zh" => CHINESE_SC_FONT_PATH,
"zh-TW" => CHINESE_TC_FONT_PATH,
_ => JAPANESE_FONT_PATH,
};
self.font_fetch_kind = Some(kind);
let slot = self.font_result.clone();
let ctx = ctx.clone();
let path = path.to_owned();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
let res = fetch_font_bytes(&path).await.map(|bytes| (path, bytes));
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
@@ -647,6 +787,8 @@ mod web {
impl eframe::App for Shell {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// First: a language another tab chose fetches its font this frame.
self.load_other_tabs_prefs();
self.tick_font(ctx);
self.app.logic(ctx);
self.api.poll();
@@ -689,6 +831,7 @@ mod web {
self.recovery_prompt(&ctx);
}
self.watch_text_for_font(&ctx);
self.store_prefs(&ctx);
}
fn persist_egui_memory(&self) -> bool {
self.launch.persists()
@@ -697,11 +840,8 @@ mod web {
if !self.launch.persists() {
return;
}
// Persist the user's own language, not a session-only English fallback.
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.app.prefs());
self.app.preferences.language = live;
let prefs = self.persisted_prefs();
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs);
}
}
@@ -822,6 +962,12 @@ mod web {
let (sender, receiver) = std::sync::mpsc::channel();
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
app.embedded = launch.embed;
// Listening before reading: settings another tab writes from now on are
// newer than those read here.
let other_tab_prefs = PrefsInbox::default();
if launch.persists() {
hear_settings_from_other_tabs(&window, &cc.egui_ctx, other_tab_prefs.clone());
}
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.load_prefs(&p);
}
@@ -862,9 +1008,14 @@ mod web {
font: Default::default(),
font_result: Rc::new(std::cell::RefCell::new(None)),
cjk_bytes: None,
chinese_bytes: None,
chinese_kind: None,
font_fetch_kind: None,
font_language,
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
prefs_writer: crate::PrefsWriter::default(),
other_tab_prefs,
}))
}),
)
@@ -1001,4 +1152,28 @@ mod tests {
assert!(!closes_design("w", false, false, false, true));
assert!(!closes_design("s", true, false, false, true));
}
/// A tab writes its settings when they change, and not again while they stay the same; the
/// settings it loaded from another tab aren't written back (#8644).
#[test]
fn changed_settings_are_written_once_and_loaded_ones_not_at_all() {
let mut writer = PrefsWriter::default();
assert_eq!(writer.take(r#"{"a":1}"#.into()).as_deref(), Some(r#"{"a":1}"#), "the first settings are written");
assert_eq!(writer.take(r#"{"a":1}"#.into()), None, "the same settings are not written again");
assert_eq!(writer.take(r#"{"a":2}"#.into()).as_deref(), Some(r#"{"a":2}"#), "a change is written at once");
writer.loaded(r#"{"a":3}"#.into());
assert_eq!(writer.take(r#"{"a":3}"#.into()), None, "settings from another tab are not written back");
assert_eq!(writer.take(r#"{"a":4}"#.into()).as_deref(), Some(r#"{"a":4}"#), "this tab's next change is written");
}
/// Only the settings another tab wrote are loaded: not other keys, and not settings taken
/// away by clearing the site's data (#8644).
#[test]
fn only_settings_another_tab_wrote_are_loaded() {
let key = solidcraft_ui_egui::prefs::STORAGE_KEY;
assert_eq!(settings_written(Some(key), Some("{}".into())).as_deref(), Some("{}"));
assert_eq!(settings_written(Some("solidcraft.webgl"), Some("1".into())), None);
assert_eq!(settings_written(Some(key), None), None, "removed settings leave this tab's");
assert_eq!(settings_written(None, None), None, "clearing all storage leaves this tab's settings");
}
}
+7
View File
@@ -28,6 +28,8 @@ neg_cmp_op_on_partial_ord = "allow"
[dependencies]
solidcraft-engine = { workspace = true }
# The D3D12 shader compiler the window starts with (#6756).
solidcraft-gpu = { workspace = true }
solidcraft-ui-egui = { workspace = true }
solidcraft-space-mouse = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
@@ -102,5 +104,10 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
raw-window-handle = "0.6"
x11rb = { version = "0.13", default-features = false }
[target.'cfg(target_os = "linux")'.dependencies]
# The desktop's dark or light preference through the XDG Desktop Portal (src/desktop_theme.rs,
# #7944); already in the tree through eframe's accessibility support (accesskit_unix).
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
[build-dependencies]
embed-resource = "3.0.11"
+16 -1
View File
@@ -4,6 +4,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let icon = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons/app/windows/exports/solidcraft-executable.ico");
println!("cargo:rerun-if-changed={}", icon.display());
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
return Ok(());
}
@@ -11,7 +12,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let copy = out.join("solidcraft.ico");
std::fs::copy(icon, &copy)?;
let rc = out.join("solidcraft.rc");
std::fs::write(&rc, format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/")))?;
let icon_line = format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/"));
std::fs::write(&rc, format!("{icon_line}{}", version_resource(&std::env::var("CARGO_PKG_VERSION")?)))?;
embed_resource::compile_for(&rc, ["solidcraft"], embed_resource::NONE).manifest_required()?;
Ok(())
}
/// The Windows version resource (what Explorer's Details tab shows and what a launcher reads).
/// `FileVersion` and `ProductVersion` are the package version verbatim (`0.25.1`, `0.26.0-rc.1`):
/// ArtCraft's launcher parses the `ProductVersion` string as semver, so a fourth `.0` would not
/// read. The binary `FILEVERSION` fields need four numbers and take the leading `major.minor.patch`.
fn version_resource(version: &str) -> String {
let mut numbers = version.split(['-', '+']).next().unwrap_or("").split('.').map(|p| p.parse::<u16>().unwrap_or(0));
let mut next = || numbers.next().unwrap_or(0);
let (major, minor, patch) = (next(), next(), next());
format!(
"1 VERSIONINFO\nFILEVERSION {major},{minor},{patch},0\nPRODUCTVERSION {major},{minor},{patch},0\nFILEOS 0x40004\nFILETYPE 0x1\n{{\nBLOCK \"StringFileInfo\"\n{{\nBLOCK \"040904B0\"\n{{\nVALUE \"CompanyName\", \"SolidCraft contributors\"\nVALUE \"FileDescription\", \"SolidCraft, parametric 3D CAD\"\nVALUE \"FileVersion\", \"{version}\"\nVALUE \"InternalName\", \"solidcraft\"\nVALUE \"OriginalFilename\", \"solidcraft.exe\"\nVALUE \"ProductName\", \"SolidCraft\"\nVALUE \"ProductVersion\", \"{version}\"\n}}\n}}\nBLOCK \"VarFileInfo\"\n{{\nVALUE \"Translation\", 0x0409, 1200\n}}\n}}\n"
)
}
+255 -32
View File
@@ -22,11 +22,15 @@ const MAX_BAD_LINES: usize = 8;
/// How long a subscribed connection waits after a `design.changed` line before the next: what
/// changes meanwhile (a parameter dragged) goes out as one line, the latest (#6985).
const EVENT_GAP: Duration = Duration::from_millis(100);
/// How long a new connection has to show the token. A client connects when it has a request to
/// send, so this is plenty; one that doesn't send it loses the connection, and its seat among
/// [`MAX_CONNECTIONS`] goes to someone else.
const TOKEN_WAIT: Duration = Duration::from_secs(5);
static OPEN: AtomicUsize = AtomicUsize::new(0);
/// 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());
/// The token files this launch wrote (its control port, the agent chat's endpoint) and the
/// token in each, removed when the app ends unless another launch has written the file since.
static TOKEN_FILES: std::sync::Mutex<Vec<(std::path::PathBuf, String)>> = std::sync::Mutex::new(Vec::new());
/// A new random token: 32 bytes from the operating system, as hex.
fn new_token() -> Option<String> {
@@ -35,20 +39,21 @@ fn new_token() -> Option<String> {
Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
/// Remove this launch's token file (at exit).
/// Remove this launch's token files and discovery file (at exit), leaving any that another
/// launch has written since (#8794).
pub fn remove_token_file() {
if let Ok(files) = TOKEN_FILES.lock() {
for p in files.iter() {
control_auth::remove_token_file(p);
for (path, token) in files.iter() {
control_auth::remove_token_file(path, token);
}
}
if let Some(port) = APP_PORT.lock().ok().and_then(|p| *p) {
control_auth::remove_current_port(port);
if let Some((port, token)) = APP_PORT.lock().ok().and_then(|p| p.clone()) {
control_auth::remove_current_port(port, &token);
}
}
/// 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);
/// The port the app's own channel listens on and its token, for removing the discovery file at exit.
static APP_PORT: std::sync::Mutex<Option<(u16, String)>> = std::sync::Mutex::new(None);
pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>();
@@ -61,7 +66,7 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
};
match listener {
Ok(l) => {
if let Some(port) = listen_on_listener(l, token, tx, ctx, false) {
if let Some(port) = listen_on_listener(l, token.clone(), 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)");
}
@@ -69,7 +74,7 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
match control_auth::write_current_port(port) {
Ok(_) => {
if let Ok(mut p) = APP_PORT.lock() {
*p = Some(port);
*p = Some((port, token));
}
}
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
@@ -112,7 +117,7 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
Ok(()) => {
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
if let Ok(mut files) = TOKEN_FILES.lock() {
files.push(path);
files.push((path, token.clone()));
}
}
Err(e) => eprintln!(
@@ -224,23 +229,53 @@ fn unauthorized() -> Value {
/// Refuse the connection: the reply, then the end of what we send, then what the client had
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
fn refuse(out: &mut TcpStream, reader: &mut BufReader<ClientInput>) {
use std::io::Read;
let _ = send(out, &unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
reader.get_mut().deadline = Some(Instant::now() + Duration::from_millis(500));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
}
/// What a client sends. Until it shows the token, all of its reads share one deadline, so neither
/// blank lines nor a line sent a byte at a time buy it more time. After that it may stay idle as
/// long as it likes.
struct ClientInput {
socket: TcpStream,
deadline: Option<Instant>,
}
impl ClientInput {
/// No deadline any more: the client showed the token.
fn admit(&mut self) -> std::io::Result<()> {
self.deadline = None;
self.socket.set_read_timeout(None)
}
}
impl std::io::Read for ClientInput {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if let Some(deadline) = self.deadline {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(std::io::ErrorKind::TimedOut.into());
}
self.socket.set_read_timeout(Some(left))?;
}
self.socket.read(buf)
}
}
/// How long a request waits for the app's reply: most requests, and a batch (or every request,
/// on a patient connection).
/// on a patient connection); and how long a new connection has to show the token.
#[derive(Clone, Copy)]
struct Waits {
short: Duration,
long: Duration,
token: Duration,
}
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600) };
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600), token: TOKEN_WAIT };
/// A request that timed out but is still running in the app (#1618): its id and method, and where
/// its reply will land.
@@ -412,11 +447,37 @@ impl Lines {
}
}
/// A subscribed connection's seat on the watch: it leaves exactly once, whichever thread sees
/// the connection's end first — the connection's own thread at the end of the stream, or the
/// events thread at the first write a closed client refuses. The seat used to wait for the
/// events thread alone, and that thread only leaves its wait when the design moves, so the
/// count could outlive the connection by a second or more (#8315).
struct Seat(Mutex<Option<Subscriber>>);
impl Seat {
fn new(counted: Subscriber) -> Self {
Seat(Mutex::new(Some(counted)))
}
/// Leave the watch now: the seat goes once, whoever gets here first.
fn release(&self) {
let counted = self.0.lock().unwrap_or_else(PoisonError::into_inner).take();
drop(counted);
}
}
impl Drop for Seat {
fn drop(&mut self) {
self.release();
}
}
/// A connection's `control.subscribe` (#6985): its `design.changed` lines, sent from a thread of
/// their own until the connection ends.
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
seat: Arc<Seat>,
}
impl Subscription {
@@ -432,15 +493,17 @@ impl Subscription {
) -> Option<(Self, std::io::Result<()>)> {
let lines = Arc::new(Lines { socket: out.try_clone().ok()?, turn: Mutex::new(()) });
let stop = Arc::new(AtomicBool::new(false));
let seat = Arc::new(Seat::new(counted));
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
let (l, s) = (lines.clone(), stop.clone());
let (l, s, watch_seat) = (lines.clone(), stop.clone(), Arc::clone(&seat));
// The connection's seat on the watch rides the events thread: it leaves when the thread
// does, so the next failed write to a closed client retires it even while the connection's
// own thread is slow to see the end of the stream (#7226).
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, counted)).ok()?;
// own thread is slow to see the end of the stream (#7226). The connection's own thread
// leaves it too, so a closed connection stops counting at once (#8315).
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, watch_seat)).ok()?;
let sent = send(&mut &lines.socket, reply);
drop(first);
Some((Subscription { lines, stop }, sent))
Some((Subscription { lines, stop, seat }, sent))
}
}
@@ -453,6 +516,9 @@ fn design_of(reply: &Value) -> Option<(String, u64)> {
impl Drop for Subscription {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
// The seat on the watch goes now: the connection is over, and nothing counts it any more
// (#8315).
self.seat.release();
// The socket closes now, not when the thread next looks; a line blocked on a client that
// stopped reading ends too.
let _ = self.lines.socket.shutdown(std::net::Shutdown::Both);
@@ -460,10 +526,10 @@ impl Drop for Subscription {
}
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
/// last one it was told of (`seen`), until `stop`. Owns the connection's seat on the watch
/// (`counted`), so the seat leaves exactly when this thread does: on `stop`, or on the first
/// write a closed client refuses (#7226).
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), _counted: Subscriber) {
/// last one it was told of (`seen`), until `stop`. Holds the connection's seat on the watch
/// (`seat`), which it leaves when the thread ends: on `stop`, or on the first write a closed
/// client refuses (#7226).
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), seat: Arc<Seat>) {
while !stop.load(Ordering::Relaxed) {
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
@@ -472,6 +538,17 @@ fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen:
seen = now;
std::thread::sleep(EVENT_GAP);
}
// The client is gone: it is not counted on the watch any more, whatever the connection's own
// thread still has to notice (#8315).
seat.release();
}
/// What this app's control channel does beyond the methods every version has, for a client to
/// find out before it relies on it (an older app answers `control.capabilities` with "unknown
/// method"). `param_check`: the levels `engine.execute`, `engine.batch` and `engine.script` take
/// as a request's own parameter check (#9184).
fn capabilities() -> Value {
json!({"param_check": solidcraft_engine::schema::ParamCheck::ALL.map(|l| l.name())})
}
/// Write `reply` on the connection: through its subscription once it has one.
@@ -500,7 +577,7 @@ fn serve_with(
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);
let mut reader = BufReader::new(ClientInput { socket: read, deadline: Some(Instant::now() + waits.token) });
let mut bad = 0usize;
let mut authenticated = false;
let mut pending: Option<Pending> = None;
@@ -511,6 +588,10 @@ fn serve_with(
let line = match read_line(&mut reader, MAX_LINE) {
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
Ok(None) => break,
// Closed without a word: a client that connected early and only now sends its first
// request would read a line here as the reply to it. It finds the connection closed
// and opens another.
Err(e) if !authenticated && matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => break,
Err(e) => {
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
break;
@@ -522,7 +603,12 @@ fn serve_with(
// The first request must carry the token; later ones on the same connection may.
if !authenticated {
match token_of(&line) {
Some(t) if control_auth::tokens_match(&t, token) => authenticated = true,
Some(t) if control_auth::tokens_match(&t, token) => {
if reader.get_mut().admit().is_err() {
break;
}
authenticated = true;
}
_ => {
refuse(&mut out, &mut reader);
break;
@@ -534,15 +620,20 @@ fn serve_with(
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). `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() {
// A request that timed out may have finished since (#1618). `control.running` and
// `control.capabilities` only look, so they leave that reply where it is.
let late = pending.as_ref().filter(|_| !matches!(method.as_str(), "control.running" | "control.capabilities")).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.running" {
json!({"ok": true, "result": running.result(connection)})
} else if method == "control.capabilities" {
json!({"ok": true, "result": capabilities()})
} else if method == "control.pending" {
match (pending.take(), late) {
(None, _) => json!({"ok": true, "result": {"pending": null}}),
@@ -627,6 +718,80 @@ fn serve_with(
mod tests {
use super::*;
/// A connection that doesn't show the token in time is closed without a word, however it fills
/// the time. One that does keeps working when it idles for longer than that.
#[test]
fn the_token_has_a_time_limit_and_an_authenticated_client_may_idle() {
use std::io::Read;
let token = "0123456789abcdef0123456789abcdef";
let limit = Duration::from_millis(500);
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
std::thread::spawn(move || {
for req in rx {
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
});
static RUNNING: Running = Running::new();
let waits = Waits { short: Duration::from_secs(10), long: Duration::from_secs(10), token: limit };
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, &DESIGN_WATCH));
}
});
let open = || {
let s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(20))).unwrap();
s
};
// Sends `drip` every 50 ms until the server closes; what it read, and when it closed.
let closed_after = |drip: &[u8]| {
let mut s = open();
let start = Instant::now();
s.set_read_timeout(Some(Duration::from_millis(50))).unwrap();
let mut got = Vec::new();
loop {
let mut buf = [0u8; 256];
match s.read(&mut buf) {
Ok(0) => break,
Ok(n) => got.extend_from_slice(buf.get(..n).unwrap()),
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
assert!(start.elapsed() < Duration::from_secs(20), "still open");
let _ = s.write_all(drip);
}
Err(_) => break,
}
}
(got, start.elapsed())
};
for drip in [&b""[..], b"\n", b"\r\n \n", b" ", b"{"] {
let (got, took) = closed_after(drip);
assert!(got.is_empty(), "{drip:?} was answered: {}", String::from_utf8_lossy(&got));
assert!(took >= limit.saturating_sub(Duration::from_millis(50)), "{drip:?} closed after {took:?}");
}
let mut s = open();
let mut lines = BufReader::new(s.try_clone().unwrap());
let mut ask = |line: Value| -> Value {
writeln!(s, "{line}").unwrap();
let mut l = String::new();
lines.read_line(&mut l).unwrap();
serde_json::from_str(&l).unwrap()
};
assert_eq!(ask(json!({"id": 1, "method": "ui.view", "token": token}))["ok"], true);
std::thread::sleep(limit * 3);
assert_eq!(ask(json!({"id": 2, "method": "ui.view"}))["ok"], true, "idle for longer than the limit");
// A wrong token is still answered, at once.
let mut wrong = open();
writeln!(wrong, "{}", json!({"id": 1, "method": "ui.view", "token": "nope"})).unwrap();
let mut l = String::new();
BufReader::new(wrong).read_line(&mut l).unwrap();
assert!(l.contains("unauthorized"), "{l}");
}
/// #1618: a request that outlasts its wait says it is still running; the connection keeps its
/// reply for `control.pending`, and refuses other requests as `busy` until it lands, so none
/// is queued to run unseen behind it.
@@ -650,7 +815,7 @@ mod tests {
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
});
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100) };
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100), token: TOKEN_WAIT };
static RUNNING: Running = Running::new();
std::thread::spawn(move || {
if let Some(stream) = listener.incoming().flatten().next() {
@@ -716,7 +881,7 @@ mod tests {
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
});
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60) };
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60), token: TOKEN_WAIT };
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
@@ -749,6 +914,9 @@ mod tests {
let mut watcher = open();
let (r, rows) = running(&mut watcher, &|_| true);
assert!(r["ok"] == true && rows.is_empty(), "idle: {r}");
// What the app's control channel does beyond the methods every version has (#9184).
let r = watcher(json!({"id": 0, "method": "control.capabilities", "token": token}));
assert_eq!((&r["ok"], &r["result"]["param_check"]), (&json!(true), &json!(["off", "warn", "strict"])), "{r}");
let mut export = open();
let path = "/private/part.step";
@@ -780,6 +948,9 @@ mod tests {
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}");
// So may it ask what the app can do: answered at once, not refused as busy.
let r = export(json!({"id": 8, "method": "control.capabilities"}));
assert_eq!(r["result"]["param_check"], json!(["off", "warn", "strict"]), "{r}");
gate_tx.send(()).unwrap();
assert_eq!(queued.join().unwrap()["result"], "engine.batch");
@@ -885,6 +1056,58 @@ mod tests {
assert!(!WATCH.subscribed());
}
/// #8315: a closed connection leaves the watch at once. The seat of a subscribed connection
/// goes when the connection's own thread sees the end of the stream, not only when the
/// events thread notices: that thread waits for the design to move, so a connection that
/// closed while the design stood still kept the watch counting it for a second or more.
#[test]
fn a_closed_connection_leaves_the_watch_at_once() {
static RUNNING: Running = Running::new();
static WATCH: DesignWatch = DesignWatch::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 names the design once and then leaves it alone: nothing moves it, so the
// events thread has nothing to write and only the end of the connection can retire it.
std::thread::spawn(move || {
for req in rx {
if req.method == "control.subscribe" {
WATCH.publish("d-1", 3);
let _ = req.reply.send(json!({"ok": true, "result": {"document_id": "d-1", "revision": 3}}));
} else {
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
}
});
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, &WATCH));
}
});
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);
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
let mut l = String::new();
lines.read_line(&mut l).unwrap();
let reply: Value = serde_json::from_str(&l).unwrap();
assert!(reply["ok"] == true, "{reply}");
assert!(WATCH.subscribed(), "the connection is subscribed");
// Let the events thread settle into its wait: the design stands still, so it has nothing
// to write, and only the end of the connection can retire it.
std::thread::sleep(Duration::from_millis(200));
// The client goes away without asking anything else.
drop((out, lines));
let deadline = Instant::now() + Duration::from_millis(400);
while WATCH.subscribed() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(!WATCH.subscribed(), "a closed connection is not counted on the watch");
}
/// 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]
+195
View File
@@ -0,0 +1,195 @@
//! Linux: the desktop's dark or light preference, for Theme: System (#7944).
//!
//! The window toolkit does not pass it on everywhere: on GNOME (Wayland, or X11 through XWayland)
//! the window never learns it, nor when it changes, so the app kept its colours. This reads the
//! freedesktop setting through the XDG Desktop Portal's Settings interface on the session bus
//! (`org.freedesktop.appearance color-scheme`, then GNOME's `color-scheme` and `gtk-theme`), the
//! same setting other applications follow, and inside a Flatpak too. It runs on a thread of its
//! own: the first reading, then the portal's `SettingChanged` signal; where the signal can't be
//! had, a reading every few seconds. Without a portal it asks `gsettings` instead, on the same
//! thread. The window reads the last answer with an atomic load
//! (`solidcraft_ui_egui::Services::desktop_dark`), and the decision from the raw values is
//! `solidcraft_ui_egui::prefs::desktop_prefers_dark`.
use std::io::Read;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
use zbus::zvariant::{OwnedValue, Value};
const UNKNOWN: u8 = 0;
const DARK: u8 = 1;
const LIGHT: u8 = 2;
/// How often to read again where the portal's change signal is not available.
const POLL: Duration = Duration::from_secs(5);
/// How long one `gsettings` reading may take before it is given up.
const GSETTINGS_TIMEOUT: Duration = Duration::from_secs(2);
const PORTAL: &str = "org.freedesktop.portal.Desktop";
const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
const SETTINGS: &str = "org.freedesktop.portal.Settings";
const APPEARANCE: &str = "org.freedesktop.appearance";
const GNOME: &str = "org.gnome.desktop.interface";
/// Start following the desktop's preference; the closure answers the latest reading (`None`
/// until there is one, or when the desktop says nothing).
pub fn start(ctx: &egui::Context) -> Box<dyn Fn() -> Option<bool>> {
let state = Arc::new(AtomicU8::new(UNKNOWN));
let (thread_state, ctx) = (state.clone(), ctx.clone());
if let Err(e) = std::thread::Builder::new().name("desktop-theme".into()).spawn(move || follow(&thread_state, &ctx)) {
log::warn!("desktop theme: no thread to follow it: {e}");
}
Box::new(move || match state.load(Ordering::Relaxed) {
DARK => Some(true),
LIGHT => Some(false),
_ => None,
})
}
/// Keep `state` on the desktop's preference, asking for a frame whenever it changes.
fn follow(state: &AtomicU8, ctx: &egui::Context) {
let publish = |dark: Option<bool>| {
let code = match dark {
Some(true) => DARK,
Some(false) => LIGHT,
None => UNKNOWN,
};
if state.swap(code, Ordering::Relaxed) != code {
ctx.request_repaint();
}
};
let portal = match portal() {
Ok(p) => Some(p),
Err(e) => {
log::info!("desktop theme: no settings portal ({e}); asking gsettings");
None
}
};
// The portal first; gsettings where there is none or it says nothing (its backend missing).
let read = |portal: Option<&zbus::blocking::Proxy<'static>>| portal.and_then(read_portal).or_else(read_gsettings);
let from_portal = portal.as_ref().and_then(read_portal);
let first = from_portal.or_else(read_gsettings);
publish(first);
// Wait for the portal's signal only when the portal answers: else the reading is gsettings',
// which only a new reading notices.
if let Some(p) = portal.as_ref().filter(|_| from_portal.is_some()) {
match p.receive_signal("SettingChanged") {
Ok(changes) => {
for message in changes {
if relevant(&message) {
publish(read(Some(p)));
}
}
log::info!("desktop theme: the portal's change signal ended; reading every few seconds");
}
Err(e) => log::info!("desktop theme: no change signal ({e}); reading every few seconds"),
}
} else if portal.is_none() && first.is_none() && !gsettings_available() {
// Neither the portal nor gsettings: nothing to follow; the window toolkit's theme stays.
return;
}
loop {
std::thread::sleep(POLL);
publish(read(portal.as_ref()));
}
}
/// The portal's Settings interface on the session bus.
fn portal() -> zbus::Result<zbus::blocking::Proxy<'static>> {
let connection = zbus::blocking::Connection::session()?;
zbus::blocking::Proxy::new(&connection, PORTAL, PORTAL_PATH, SETTINGS)
}
/// One reading through the portal.
fn read_portal(portal: &zbus::blocking::Proxy<'static>) -> Option<bool> {
let appearance = read_setting(portal, APPEARANCE, "color-scheme").and_then(|v| as_u32(&v));
// The GNOME settings only matter when the freedesktop one has no preference.
if matches!(appearance, Some(1 | 2)) {
return solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, None, None);
}
let gnome = read_setting(portal, GNOME, "color-scheme").and_then(|v| as_string(&v));
let gtk_theme = read_setting(portal, GNOME, "gtk-theme").and_then(|v| as_string(&v));
solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, gnome.as_deref(), gtk_theme.as_deref())
}
/// A setting through `ReadOne` (portal version 2), else the older `Read`.
fn read_setting(portal: &zbus::blocking::Proxy<'static>, namespace: &str, key: &str) -> Option<OwnedValue> {
let one: zbus::Result<OwnedValue> = portal.call("ReadOne", &(namespace, key));
one.or_else(|_| -> zbus::Result<OwnedValue> { portal.call("Read", &(namespace, key)) }).ok()
}
/// Whether a `SettingChanged` signal is about a setting the preference is read from.
fn relevant(message: &zbus::Message) -> bool {
let body = message.body();
let Ok((namespace, key, _)) = body.deserialize::<(String, String, OwnedValue)>() else { return false };
matches!((namespace.as_str(), key.as_str()), (APPEARANCE, "color-scheme") | (GNOME, "color-scheme" | "gtk-theme"))
}
/// The value inside the variants `Read` wraps it in (a portal answers one or two deep).
fn unwrap_variants<'v, 'a>(value: &'v Value<'a>) -> &'v Value<'a> {
let mut value = value;
for _ in 0..4 {
match value {
Value::Value(inner) => value = &**inner,
_ => break,
}
}
value
}
fn as_u32(value: &Value<'_>) -> Option<u32> {
match unwrap_variants(value) {
Value::U32(n) => Some(*n),
Value::I32(n) => u32::try_from(*n).ok(),
Value::U8(n) => Some(u32::from(*n)),
_ => None,
}
}
fn as_string(value: &Value<'_>) -> Option<String> {
match unwrap_variants(value) {
Value::Str(s) => Some(s.as_str().to_string()),
_ => None,
}
}
/// Without a portal: GNOME's settings through `gsettings` (`None` when it is not there).
fn read_gsettings() -> Option<bool> {
let gnome = gsettings("color-scheme");
let gtk_theme = gsettings("gtk-theme");
solidcraft_ui_egui::prefs::desktop_prefers_dark(None, gnome.as_deref(), gtk_theme.as_deref())
}
fn gsettings_available() -> bool {
gsettings("gtk-theme").is_some()
}
/// `gsettings get org.gnome.desktop.interface <key>`, its quoted answer, or `None` when the
/// program is missing, fails or does not answer in time.
fn gsettings(key: &str) -> Option<String> {
let mut child = std::process::Command::new("gsettings")
.args(["get", GNOME, key])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
let started = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => break,
Ok(None) if started.elapsed() < GSETTINGS_TIMEOUT => std::thread::sleep(Duration::from_millis(20)),
_ => {
let _ = child.kill();
let _ = child.wait();
return None;
}
}
}
let mut out = String::new();
child.stdout.take()?.take(4096).read_to_string(&mut out).ok()?;
let value = out.trim().trim_matches('\'').to_string();
(!value.is_empty()).then_some(value)
}
+93 -10
View File
@@ -5,12 +5,25 @@
//! 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.
//!
//! The old process ends without tearing down its GPU objects ([`end`]): on a lost NVIDIA device
//! destroying the window's swap chain busy-waits in the driver and never returns (#9044).
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";
/// Set on the start after a lost device: why it was lost, as wgpu said it. The old process's
/// session journal, which recorded it, is gone by then.
pub const REASON_ENV: &str = "SOLIDCRAFT_DEVICE_LOST_REASON";
/// The longest reason passed on ([`REASON_ENV`]) and shown, in characters.
const REASON_CHARS: usize = 300;
/// How long the process may take to end ([`end`]) before it is aborted.
const END_GRACE: std::time::Duration = std::time::Duration::from_secs(3);
/// 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.
@@ -32,27 +45,54 @@ pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool
after_recent_loss || cpu_renderer
}
/// `reason` on one line, at most [`REASON_CHARS`] long; None when there is nothing to say.
pub fn reason(reason: Option<&str>) -> Option<String> {
let line = reason?.split_whitespace().collect::<Vec<_>>().join(" ");
(!line.is_empty()).then(|| line.chars().take(REASON_CHARS).collect())
}
/// What the start after a lost device tells the user, `software` when it runs on the software
/// renderer.
pub fn note(software: bool) -> String {
/// renderer, with what the driver reported ([`REASON_ENV`]).
pub fn note(software: bool, reason: Option<&str>) -> 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.");
}
if let Some(reason) = self::reason(reason) {
note.push_str(&format!(" The graphics driver reported: {reason}"));
}
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 };
/// The command that starts `exe` again with `args`, the loss stamped at `lost_at` and its
/// `reason`, on the software renderer when `software`.
fn command(
exe: std::path::PathBuf,
args: impl IntoIterator<Item = std::ffi::OsString>,
software: bool,
lost_at: u64,
reason: &str,
) -> std::process::Command {
let mut command = std::process::Command::new(exe);
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
command.args(args).env(LOST_ENV, lost_at.to_string());
match self::reason(Some(reason)) {
Some(reason) => command.env(REASON_ENV, reason),
// Not one inherited from an earlier loss.
None => command.env_remove(REASON_ENV),
};
if software {
command.env(startup::RENDERER_ENV, "cpu");
}
command
}
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at` and its `reason`,
/// on the software renderer when `software`.
pub fn restart(software: bool, lost_at: u64, reason: &str) {
let Ok(exe) = std::env::current_exe() else { return };
let mut command = command(exe, std::env::args_os().skip(1), software, lost_at, reason);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
@@ -64,6 +104,23 @@ pub fn restart(software: bool, lost_at: u64) {
}
}
/// End the process now, without tearing down what is left on the lost device: the window's swap
/// chain, the viewport's buffers and pipelines. On NVIDIA's Vulkan driver destroying the swap
/// chain of a lost device busy-waits forever, which left the old window frozen at 100% of a core
/// beside the new one (#9044). Should the exit itself get stuck in a library's exit handler, the
/// process is aborted after [`END_GRACE`].
pub fn end() -> ! {
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
std::thread::sleep(END_GRACE);
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
std::process::abort();
});
if let Err(e) = watchdog {
log::warn!("graphics: no exit watchdog: {e}");
}
std::process::exit(0)
}
#[cfg(test)]
mod tests {
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
@@ -84,8 +141,34 @@ mod tests {
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"));
assert!(super::note(true, None).contains("software rendering"));
assert!(!super::note(false, None).contains("software rendering"));
assert!(super::note(false, None).contains("kept for recovery"));
}
/// The start after a loss is told why (#9044): the old process's journal held the reason, and
/// it is deleted as that process closes.
#[test]
fn the_next_start_is_told_why_the_device_was_lost() {
let exe = std::path::PathBuf::from("solidcraft");
let args = ["--control".into(), "7979".into()];
let command = super::command(exe.clone(), args, true, 1_000, "Lost: Device lost\n (vkQueueSubmit)");
let env = |command: &std::process::Command, name: &str| {
command.get_envs().find(|(k, _)| *k == name).map(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
};
assert_eq!(env(&command, super::LOST_ENV), Some(Some("1000".into())));
assert_eq!(env(&command, super::REASON_ENV), Some(Some("Lost: Device lost (vkQueueSubmit)".into())), "on one line");
assert_eq!(env(&command, crate::startup::RENDERER_ENV), Some(Some("cpu".into())));
assert_eq!(command.get_args().collect::<Vec<_>>(), ["--control", "7979"]);
// No reason: none inherited from an earlier loss either.
let command = super::command(exe, Vec::new(), false, 1_000, " ");
assert_eq!(env(&command, super::REASON_ENV), Some(None));
assert_eq!(env(&command, crate::startup::RENDERER_ENV), None);
let note = super::note(false, Some("Lost: Device lost"));
assert!(note.ends_with("The graphics driver reported: Lost: Device lost"), "{note}");
assert!(!super::note(false, Some(" \n ")).contains("reported"));
let long = "x".repeat(1_000);
assert_eq!(super::reason(Some(&long)).map(|r| r.chars().count()), Some(300));
}
}
+317 -10
View File
@@ -22,6 +22,8 @@
mod app_icon;
mod come_forward;
mod control_server;
#[cfg(target_os = "linux")]
mod desktop_theme;
mod device_lost;
#[cfg(target_os = "windows")]
mod native_crash;
@@ -97,6 +99,86 @@ struct App {
nav_bridge: nav_bridge::NavBridge,
/// Where the next file dialog opens: it follows the open design (#7756).
dialog_folder: DialogFolder,
/// Settings go to `app.ron` in the frame they change (#8275).
prefs_writer: PrefsWriter,
}
/// Settings written to the settings file soon after they change (#8275). eframe saves them
/// only every 30 s and when the window closes normally, so a change was lost whenever the app
/// did not get that far: ended from the Task Manager, stopped by a sign-out or shutdown, or
/// crashed or hung in the graphics driver on the way out.
///
/// Some settings change continuously (a panel's width while its edge is dragged), so the file
/// is written at most once per [`PrefsWriter::INTERVAL`], and a change made in between is written
/// when that interval ends.
#[derive(Default)]
struct PrefsWriter {
/// The settings written (or read at start) last.
written: Option<String>,
/// The previous frame had input that can change settings: what it changed shows in this
/// frame's settings.
input_before: bool,
/// When the settings were last written by this writer.
last_write: Option<std::time::Instant>,
/// A change waits until then to be written.
pending_until: Option<std::time::Instant>,
}
impl PrefsWriter {
/// The least time between two writes of the settings file.
const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
/// The settings as they were read at start: they are on disk already.
fn read(prefs: String) -> Self {
Self { written: Some(prefs), ..Self::default() }
}
/// Whether this frame's settings are worth comparing: input that can change them came in this
/// frame or the one before, or a waiting change's time has come. Pointer motion alone isn't
/// such input; eframe's own save still catches anything else.
fn due(&mut self, input: bool, now: std::time::Instant) -> bool {
let due = input || self.input_before || self.pending_until.is_some_and(|at| now >= at);
self.input_before = input;
due
}
/// Write `prefs` and flush them to disk if they differ from the settings written last and the
/// last write is at least [`Self::INTERVAL`] old. Returns how long a change waits otherwise:
/// the caller asks for a frame then, so an idle app still writes it.
fn store(&mut self, storage: &mut dyn eframe::Storage, prefs: String, now: std::time::Instant) -> Option<std::time::Duration> {
if self.written.as_deref() == Some(prefs.as_str()) {
self.pending_until = None;
return None;
}
if let Some(wait) = self.last_write.map(|at| Self::INTERVAL.saturating_sub(now.saturating_duration_since(at))).filter(|w| !w.is_zero()) {
self.pending_until = Some(now + wait);
return Some(wait);
}
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
storage.flush();
self.written = Some(prefs);
self.last_write = Some(now);
self.pending_until = None;
None
}
}
/// Input that can change a setting: a click, a key, typed or pasted text, a wheel or a pinch.
/// Pointer motion, which comes every frame while the mouse moves, cannot by itself.
fn changes_settings(event: &egui::Event) -> bool {
matches!(
event,
egui::Event::PointerButton { .. }
| egui::Event::Key { .. }
| egui::Event::Text(_)
| egui::Event::Paste(_)
| egui::Event::Cut
| egui::Event::Ime(_)
| egui::Event::MouseWheel { .. }
| egui::Event::Zoom(_)
| egui::Event::Rotate(_)
| egui::Event::AccessKitActionRequest(_)
)
}
impl eframe::App for App {
@@ -184,11 +266,21 @@ impl eframe::App for App {
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) {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
content_ui(&mut self.solve, ui);
// After the frame's UI, so a setting changed in it (Preferences' OK) is written now.
let now = std::time::Instant::now();
if self.prefs_writer.due(ui.input(|i| i.events.iter().any(changes_settings)), now)
&& let Some(storage) = frame.storage_mut()
&& let Some(wait) = self.prefs_writer.store(storage, self.solve.prefs(), now)
{
ui.ctx().request_repaint_after(wait);
}
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
let prefs = self.solve.prefs();
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
self.prefs_writer.written = Some(prefs);
}
fn on_exit(&mut self) {
// The agent chat's agent and everything it started end with the app.
@@ -202,13 +294,29 @@ impl eframe::App for App {
}
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
// Before a restart below starts the next instance, which would otherwise get nothing from
// the 3D mouse driver (`solidcraft_3dx_client::stop`).
// Before a restart (`Drop for App`) starts the next instance, which would otherwise get
// nothing from the 3D mouse driver (`solidcraft_3dx_client::stop`).
#[cfg(target_os = "macos")]
solidcraft_3dx_client::stop();
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);
// The next start (after a lost device, or into an update) begins once the app is dropped
// (`Drop for App`), when the settings are on disk.
}
}
impl Drop for App {
/// eframe drops its settings storage first, which waits for `app.ron` to be written, then the
/// app, then the window and its GPU surface. So the next start begins here: started from
/// `on_exit`, it read `app.ron` while the old process was still writing it, found it
/// unreadable and moved it aside, resetting every setting (#9044).
fn drop(&mut self) {
if let Some(gpu) = &self.gpu
&& let Some(lost_at) = gpu.lost_at
{
device_lost::restart(gpu.restart_on_software, lost_at, &gpu.reason);
// The window's GPU surface is still to be dropped, and on a lost device that may
// never return (#9044).
device_lost::end();
}
// 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) {
@@ -236,6 +344,8 @@ struct LostWatch {
restart_on_software: bool,
/// When the loss was seen: the app is closing to start again.
lost_at: Option<u64>,
/// Why, as wgpu said it: passed to the next start ([`device_lost::REASON_ENV`]).
reason: String,
}
impl App {
@@ -249,6 +359,7 @@ impl App {
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());
gpu.reason = reason;
self.solve.quit_requested = true;
}
}
@@ -422,6 +533,12 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
window_system: window_system_now(),
// Linux: the desktop's dark or light setting, which the window is not told on every
// desktop (#7944). Windows and macOS: the window's own theme.
#[cfg(target_os = "linux")]
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
#[cfg(not(target_os = "linux"))]
desktop_dark: None,
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.
@@ -701,6 +818,24 @@ fn msaa() -> Option<u16> {
/// (`solidcraft_ui_egui::titlebar`). macOS keeps its traffic lights over the integrated bar.
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
/// Linux: the window manager's own decorations (server-side, or the compositor's frame) can be
/// asked for instead of the application bar ([`titlebar_choice`]). Elsewhere always `true`.
fn app_titlebar_wanted() -> bool {
if !cfg!(all(unix, not(target_os = "macos"))) {
return true;
}
let setting = graphics().and_then(|v| v.get("titlebar").and_then(serde_json::Value::as_str).map(str::to_string));
titlebar_choice(solidcraft_engine::geom::env::var("TITLEBAR").as_deref(), setting.as_deref())
}
/// Linux: whether the application bar is the title bar. `SOLIDCRAFT_TITLEBAR` (else
/// `graphics.json`'s `titlebar`) set to `system` gives the window the desktop's own title bar and
/// buttons; `app` (or anything else, or nothing) keeps the application bar as the title bar.
fn titlebar_choice(env: Option<&str>, setting: Option<&str>) -> bool {
let choice = env.map(str::trim).filter(|v| !v.is_empty()).or(setting.map(str::trim));
!choice.is_some_and(|v| v.eq_ignore_ascii_case("system"))
}
const DEPTH_BITS: u8 = 24;
/// A 32-bit float depth buffer, drawn with reversed depth (1 near, 0 far) so its precision follows
/// the distance from the eye (#1434). Only where every adapter offers it with a stencil
@@ -750,6 +885,16 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
mark("main");
// On OCLP-patched Macs, wgpu picks a Metal Shading Language standard that the system's
// compiler refuses (`invalid value 'metal3.2'`), because the patched OS reports a newer
// macOS version than the GPU's Metal compiler supports. Re-launching with
// SYSTEM_VERSION_COMPAT=1 makes the OS report the real (older) version so wgpu selects
// a standard the compiler accepts. The variable must be set before the process starts, so
// we detect OCLP here and re-exec immediately, before any macOS framework call (#3080, #4605).
#[cfg(target_os = "macos")]
if let Err(e) = startup::relaunch_with_version_compat() {
log::warn!("startup: OCLP check failed (continuing normally): {e}");
}
// 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() {
@@ -821,7 +966,11 @@ fn main() -> eframe::Result {
// 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));
let reason = std::env::var(device_lost::REASON_ENV).ok();
if let Some(reason) = device_lost::reason(reason.as_deref()) {
log::warn!("graphics: the last launch lost its device ({reason})");
}
notes.push(device_lost::note(cpu_renderer, reason.as_deref()));
}
if safe_mode {
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
@@ -857,7 +1006,7 @@ fn main() -> eframe::Result {
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);
let custom_titlebar = CUSTOM_TITLEBAR && app_titlebar_wanted() && !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 {
@@ -961,6 +1110,7 @@ fn main() -> eframe::Result {
lost: rs.device_lost.clone(),
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
lost_at: None,
reason: String::new(),
});
if !safe_mode {
app.preferences.graphics_adapter =
@@ -976,6 +1126,12 @@ fn main() -> eframe::Result {
})
.collect();
let info = rs.adapter.get_info();
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
eprintln!(
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
app.preferences.graphics_adapter, info.name
);
}
let samples = scene_msaa_request(saved_samples);
let supported = scene_sample_choices(depth_bits, STENCIL_BITS, |depth| {
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
@@ -1016,6 +1172,10 @@ fn main() -> eframe::Result {
app.session.doc_mut().name = "Sample Plate".into();
app.session.mark_saved();
app.session.undo.clear();
// Built straight into the session, so nothing asked for a fit: frame it at the
// home view as any opened design is (#9195). The fit waits for the first layout.
app.cam.set_view(solidcraft_engine::render::StandardView::Iso);
app.fit_view();
}
// Lock files next to open designs: a design open on another computer says so (#5467),
// the files given at launch included.
@@ -1076,6 +1236,8 @@ fn main() -> eframe::Result {
mark("app created");
solidcraft_engine::crash::phase("first frame");
Ok(Box::new(App {
// Before `app` moves: the settings as read, already on disk.
prefs_writer: PrefsWriter::read(app.prefs()),
solve: app,
starting,
frames: 0,
@@ -1099,12 +1261,13 @@ fn main() -> eframe::Result {
}
/// egui-wgpu is about to start a step of getting the graphics device and the window's surface up:
/// the crash journal keeps it, and the adapter once it is picked.
/// the crash journal keeps it until the app runs (a window opened later goes through the same
/// steps), and the adapter once it is picked.
fn startup_step(step: &'static str, detail: &str) {
if step == eframe::egui_wgpu::STEP_ADAPTER_CHOSEN {
solidcraft_engine::crash::set_environment(&format!("Renderer: {detail}"));
}
solidcraft_engine::crash::phase(step);
solidcraft_engine::crash::startup_step(step);
}
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
@@ -1124,6 +1287,8 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
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());
// Never a `dxcompiler.dll` another program put on PATH (#6756).
setup.instance_descriptor.backend_options.dx12.shader_compiler = solidcraft_gpu::dx12_compiler();
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
startup::configure_adapter(setup, cpu, environment, adapter);
@@ -1264,6 +1429,148 @@ mod backend_tests {
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
}
#[test]
fn the_desktops_title_bar_only_when_asked_for() {
use super::titlebar_choice;
// Nothing set: the application bar is the title bar, as before.
assert!(titlebar_choice(None, None));
// graphics.json's `titlebar`.
assert!(!titlebar_choice(None, Some("system")));
assert!(titlebar_choice(None, Some("app")));
// SOLIDCRAFT_TITLEBAR wins over the file; empty counts as unset; case and spaces don't matter.
assert!(!titlebar_choice(Some(" System "), Some("app")));
assert!(titlebar_choice(Some("app"), Some("system")));
assert!(!titlebar_choice(Some(""), Some("system")));
// Anything unknown keeps the application bar.
assert!(titlebar_choice(Some("ssd"), None));
}
}
#[cfg(test)]
mod prefs_writer_tests {
use super::PrefsWriter;
use solidcraft_ui_egui::prefs::{NavigatorStyle, STORAGE_KEY};
use std::collections::HashMap;
use std::time::{Duration, Instant};
/// eframe's settings file: what was flushed to disk is what the next start reads.
#[derive(Default)]
struct Disk {
pending: HashMap<String, String>,
flushed: HashMap<String, String>,
flushes: usize,
}
impl eframe::Storage for Disk {
fn get_string(&self, key: &str) -> Option<String> {
self.pending.get(key).cloned()
}
fn set_string(&mut self, key: &str, value: String) {
self.pending.insert(key.into(), value);
}
fn remove_string(&mut self, key: &str) {
self.pending.remove(key);
}
fn flush(&mut self) {
self.flushed = self.pending.clone();
self.flushes += 1;
}
}
fn new_app() -> solidcraft_ui_egui::SolveApp {
solidcraft_ui_egui::SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default())
}
/// #8275: Preferences' View navigator and Display quality, changed and confirmed, are on disk
/// in that frame, so the next start has them even when this one never closes normally (ended
/// from the Task Manager, a sign-out, a hang or crash in the graphics driver on the way out).
#[test]
fn changed_settings_reach_the_disk_in_the_frame_they_change() {
let mut app = new_app();
let mut disk = Disk::default();
let mut writer = PrefsWriter::read(app.prefs());
let now = Instant::now();
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
assert_eq!(disk.flushes, 0, "the settings read at start are not written again");
app.preferences.navigator_style = NavigatorStyle::Cube;
app.preferences.display_quality = "fine".into();
assert!(writer.due(true, now), "a frame with input is checked");
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
assert_eq!(disk.flushes, 1, "flushed at once, not left for eframe's next save");
assert_eq!(writer.store(&mut disk, app.prefs(), now + Duration::from_secs(5)), None);
assert_eq!(disk.flushes, 1, "unchanged settings are not written again");
// The next start reads them.
let mut next = new_app();
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
next.load_prefs(&stored);
assert_eq!(next.preferences.navigator_style, NavigatorStyle::Cube);
assert_eq!(next.preferences.display_quality, "fine");
assert!(disk.flushed.contains_key(STORAGE_KEY));
}
/// A frame without input is checked only right after one with input (whose clicks the UI
/// handled in that frame); after that, idle frames cost nothing. Pointer motion, which comes
/// every frame while the mouse moves, doesn't count as input.
#[test]
fn frames_are_checked_with_input_and_once_after_it() {
let now = Instant::now();
let mut writer = PrefsWriter::default();
assert!(!writer.due(false, now));
assert!(writer.due(true, now));
assert!(writer.due(false, now), "the frame after input");
assert!(!writer.due(false, now));
let click = egui::Event::PointerButton {
pos: egui::Pos2::ZERO,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::NONE,
};
assert!(super::changes_settings(&click));
assert!(super::changes_settings(&egui::Event::Text("5".into())));
assert!(!super::changes_settings(&egui::Event::PointerMoved(egui::Pos2::ZERO)));
assert!(!super::changes_settings(&egui::Event::MouseMoved(egui::Vec2::X)));
}
/// A setting that changes in every frame (a panel's width while its edge is dragged) doesn't
/// write the file in every frame: at most once a second, and the last change is on disk at
/// most a second after it was made, with a frame asked for then so an idle app writes it.
#[test]
fn a_continuous_change_is_written_once_a_second_and_its_end_soon_after() {
let mut app = new_app();
let mut disk = Disk::default();
let mut writer = PrefsWriter::read(app.prefs());
let start = Instant::now();
let frame = Duration::from_millis(16);
let mut asked = None;
// 1.5 s of dragging at 60 frames a second, with input in every frame.
for n in 0..94u32 {
let now = start + frame * n;
app.preferences.browser_width = 200.0 + n as f32;
assert!(writer.due(true, now));
asked = writer.store(&mut disk, app.prefs(), now);
}
assert_eq!(disk.flushes, 2, "the first change at once, then one a second later");
let wait = asked.expect("the last width waits, and a frame is asked for when it can be written");
assert!(wait <= PrefsWriter::INTERVAL);
// The drag ends: no more input. Frames before the wait is over don't even look.
let end = start + frame * 93;
assert!(writer.due(false, end + frame), "the frame right after input");
assert!(!writer.due(false, end + frame * 2), "an idle frame before the wait is over");
// The frame asked for writes the last width.
let then = end + wait;
assert!(writer.due(false, then));
assert_eq!(writer.store(&mut disk, app.prefs(), then), None);
assert_eq!(disk.flushes, 3);
let mut next = new_app();
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
next.load_prefs(&stored);
assert_eq!(next.preferences.browser_width, 293.0, "the width the drag ended at");
assert!(!writer.due(false, then + frame), "nothing waits any more");
}
}
#[cfg(test)]
+1
View File
@@ -101,6 +101,7 @@ impl NativeMenu {
])?;
for (action, label) in [
("docs", "Documentation"),
("updates", app.tr("update.check_for_updates")),
("shortcuts", "Keyboard Shortcuts"),
("messages", "Messages\u{2026}"),
("report", "Report an Issue\u{2026}"),
+114 -12
View File
@@ -137,8 +137,11 @@ pub struct AdapterChoice {
pub compatible: bool,
}
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
/// environment override follows wgpu's case-insensitive substring convention.
/// Choose an adapter by name. The environment override follows wgpu's case-insensitive
/// substring convention among surface-compatible adapters. A saved preference prefers an
/// exact (then case-insensitive) name among compatible adapters, and if the named card is
/// only listed as incompatible with the surface still picks it (#8341: otherwise the
/// preference is kept in the dialog while wgpu silently uses another GPU).
/// No match leaves selection to the normal wgpu request.
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
@@ -149,12 +152,14 @@ pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, pre
return Some(index);
}
}
if let Some(name) = preference.filter(|s| !s.is_empty())
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
{
return Some(index);
}
None
let name = preference.filter(|s| !s.is_empty())?;
let find = |pred: &dyn Fn(&AdapterChoice) -> bool, only_compatible: bool| {
adapters.iter().enumerate().find(|(_, a)| (!only_compatible || a.compatible) && pred(a)).map(|(i, _)| i)
};
find(&|a| a.name == name, true)
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), true))
.or_else(|| find(&|a| a.name == name, false))
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), false))
}
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
@@ -255,7 +260,7 @@ pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::
/// asks for a float one only when whichever it picks has it.
pub fn float_depth_everywhere(options: &eframe::egui_wgpu::WgpuConfiguration) -> bool {
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &options.wgpu_setup else { return false };
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle_from_env();
let mut desc = solidcraft_gpu::instance_descriptor_from_env();
desc.backends = setup.instance_descriptor.backends;
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(setup.instance_descriptor.backends));
@@ -272,6 +277,7 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = backends;
desc.flags = eframe::wgpu::InstanceFlags::from_build_config().with_env();
desc.backend_options.dx12.shader_compiler = solidcraft_gpu::dx12_compiler();
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
let found: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().device_type)).collect();
@@ -291,6 +297,71 @@ pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
}
/// The environment variable that makes the macOS compatibility shim report an older OS
/// version to wgpu's Metal backend. On OCLP-patched machines the Metal compiler accepts
/// only up to the MSL standard of the original GPU, not the one the patched OS reports,
/// so wgpu picks a standard that the compiler refuses (`invalid value 'metal3.2'`).
/// Setting this variable before the process starts causes the OS to report the older
/// version and wgpu selects a supported MSL standard (#3080, #4605).
#[cfg(target_os = "macos")]
pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
/// The NVRAM variable that OpenCore Legacy Patcher writes on every OCLP-booted system.
/// Its presence is the primary signal that `SYSTEM_VERSION_COMPAT=1` is needed.
#[cfg(target_os = "macos")]
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
/// `true` if the OCLP NVRAM key is present on this system.
///
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
/// permission denied, key absent) returns `false` — this is a best-effort probe.
#[cfg(target_os = "macos")]
pub fn oclp_nvram_present() -> bool {
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
}
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
/// the NVRAM key. The probe runs only when it is needed, so a re-launched process (variable
/// set) never starts `nvram`.
#[cfg(target_os = "macos")]
fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
!compat_set && nvram()
}
/// If this process is running under OCLP and `SYSTEM_VERSION_COMPAT` has not been set,
/// re-launch the current binary with `SYSTEM_VERSION_COMPAT=1` added to the environment,
/// then exit with the child's status code.
///
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
/// already queried the OS version by then. Re-launching is the only safe approach that
/// works without `unsafe` (a true `execve` replacement would require it).
///
/// Returns `Ok(false)` immediately when:
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
/// - the OCLP NVRAM key is absent.
///
/// Returns `Err` only when `std::env::current_exe()` fails or the child process cannot
/// be spawned; the caller should log the error and continue normally.
///
/// Never returns `Ok(true)`: a successful re-launch exits the current process.
#[cfg(target_os = "macos")]
pub fn relaunch_with_version_compat() -> Result<bool, String> {
// A set variable means this is the re-launched process: no loop.
if !compat_wanted(std::env::var_os(SYSTEM_VERSION_COMPAT_ENV).is_some(), oclp_nvram_present) {
return Ok(false);
}
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
let status = std::process::Command::new(&exe)
.args(std::env::args_os().skip(1))
.env(SYSTEM_VERSION_COMPAT_ENV, "1")
.status()
.map_err(|e| format!("could not re-launch with {}: {e}", SYSTEM_VERSION_COMPAT_ENV))?;
std::process::exit(status.code().unwrap_or(1));
}
/// 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
@@ -350,7 +421,8 @@ mod tests {
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
assert_eq!(choose_adapter(&cards, None, Some("discrete small")), Some(1), "preference matches without regard to case");
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "a preference is the full name, not a substring");
}
#[test]
@@ -359,11 +431,20 @@ mod tests {
assert_eq!(choose_adapter(&cards, None, None), None);
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
cards[2].compatible = false;
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
// Environment still needs a compatible adapter; a preference keeps the named card (#8341).
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), Some(2));
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
assert_eq!(choose_adapter(&[], None, None), None);
}
#[test]
fn preference_keeps_a_named_card_even_when_surface_says_incompatible() {
let mut cards = cards();
cards[1].compatible = false;
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, None, Some("discrete SMALL")), Some(1));
}
#[test]
fn automatic_adapter_preserves_the_previous_request_configuration() {
use eframe::egui_wgpu::WgpuSetupCreateNew;
@@ -413,6 +494,27 @@ mod tests {
assert_eq!(window_instance_flags(on, false, None), on);
}
#[cfg(target_os = "macos")]
#[test]
fn oclp_relaunch_needs_the_signal_and_no_compat_variable() {
use super::compat_wanted;
let probed = std::cell::Cell::new(false);
let probe = |found: bool| {
let probed = &probed;
move || {
probed.set(true);
found
}
};
// The re-launched process has the variable: no probe, no second re-launch.
assert!(!compat_wanted(true, probe(true)));
assert!(!probed.get());
// The OCLP NVRAM key is the signal.
assert!(compat_wanted(false, probe(true)));
// No signal: an ordinary Mac starts as it is.
assert!(!compat_wanted(false, probe(false)));
}
#[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")));
@@ -591,7 +693,7 @@ mod tests {
if cfg!(target_os = "windows") && std::env::var_os(WGPU_BACKEND_ENV).is_none() {
assert_eq!(backends, eframe::wgpu::Backends::DX12);
}
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle_from_env();
let mut desc = solidcraft_gpu::instance_descriptor_from_env();
desc.backends = backends;
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
+1 -1
View File
@@ -1,5 +1,5 @@
//! Built-in model appearances: names, sRGB colours and metalness.
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
pub const LIBRARY: &[(&str, [u8; 3], f64)] = &[
("Aluminum - Satin", [204, 208, 214], 1.0),
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
+1 -1
View File
@@ -1,7 +1,7 @@
//! 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] = [
pub const OPAQUE_FINISHES: &[(&str, [u8; 3], f64, f64)] = &[
("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),
+1 -1
View File
@@ -16,7 +16,7 @@ const fn glass(transmission_linear_rgb: [f64; 3], ior: f64, reference_path_mm: f
GlassRecipe { transmission_linear_rgb, dielectric_ior: ior, reference_path_mm, boundary }
}
pub const STARTER_PACK: [VisualPreset; 40] = [
pub const STARTER_PACK: &[VisualPreset] = &[
VisualPreset::opaque(
"starter.wood.oak-honey",
"Wood - Honey Oak Uniform Preview",
Binary file not shown.
+96
View File
@@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
Subset for SolidCraft web UI (glyphs used by the zh locale); renamed Craft UI SC (no Reserved Font Name).
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+96
View File
@@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
Subset for SolidCraft web UI (glyphs used by the zh-TW locale); renamed Craft UI TC (no Reserved Font Name).
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+8 -2
View File
@@ -188,7 +188,7 @@ surface = [
sketch = [
"arc3", "arc_center", "arc_slot", "arc_slot_center", "arc_tangent",
"blend", "circle", "circle2", "circle3", "circle_tt",
"circle_ttt", "conic", "constrainer", "dimension", "ellipse",
"circle_ttt", "conic", "constrainer", "dimension", "ellipse", "equation_curve",
"extend", "finish", "fit_section", "include",
"intersect", "intersection_curve", "iso_curve", "line", "line_mid",
"offset", "offset_two_sides", "offset_symmetric", "point", "polygon", "polygon_circumscribed", "polygon_edge",
@@ -204,5 +204,11 @@ constraint = [
"c_polygon", "c_smooth", "c_symmetry", "c_tangent",
]
construction = [
"centerline", "construction", "infinite",
"centerline", "construction", "construction_line", "construction_line_angle",
"construction_line_bisector", "construction_line_horizontal", "construction_line_parallel",
"construction_line_perpendicular", "construction_line_tangent_at", "construction_line_tangent_external",
"construction_line_tangent_two", "construction_line_vertical", "infinite",
]
mesh = [
"mesh_body",
]
@@ -70,6 +70,16 @@ configuration_table false
conic false
constrainer false
construction false
construction_line false
construction_line_angle false
construction_line_bisector false
construction_line_horizontal false
construction_line_parallel false
construction_line_perpendicular false
construction_line_tangent_at false
construction_line_tangent_external false
construction_line_tangent_two false
construction_line_vertical false
contact_all false
contact_new false
contact_off false
@@ -95,6 +105,7 @@ edit false
ellipse false
emboss false
environment_map false
equation_curve false
error false
explode false
export false
@@ -103,6 +114,7 @@ extend false
extrude false
eye false
fastener false
feature_group false
fillet false
finish false
finish_edit false
@@ -143,6 +155,7 @@ loft false
lookat false
material false
measure false
mesh_body false
mesh_export false
min_radius false
mirror false
@@ -168,6 +181,7 @@ pipe false
plane false
plane_along_path false
plane_angle false
plane_axis_point false
plane_midplane false
plane_perpendicular false
plane_tangent false
@@ -188,6 +202,7 @@ polygon false
polygon_circumscribed false
polygon_edge false
presspull false
print_face false
project false
project_surface false
rack_pinion false
@@ -222,6 +237,8 @@ settings_monitor false
settings_mouse false
settings_palette false
settings_plug false
settings_profile false
sheet false
sheet_rules false
shell false
shield false
@@ -257,6 +274,7 @@ split_face false
spun false
star false
status_error false
status_pin false
status_recovery false
style false
style_hidden_edges false
@@ -288,6 +306,8 @@ svg false
sweep false
text false
thread false
timeline_play false
timeline_stop false
torus false
trace false
trim false
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Settings profile</title>
<circle cx="32" cy="19" r="8" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 53C12 44.7157 20.9543 38 32 38C43.0457 38 52 44.7157 52 53" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Pinned result</title>
<path d="M32 57C25 48 14 35 14 24A18 18 0 0 1 50 24C50 35 39 48 32 57Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25 24A7 7 0 1 0 39 24A7 7 0 1 0 25 24Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Construction line through two points</title>
<path d="M8 52L56 12" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
<circle cx="20" cy="42" r="5.8" fill="#ffffff"/>
<circle cx="20" cy="42" r="3.4" fill="#236ead"/>
<circle cx="44" cy="22" r="5.8" fill="#ffffff"/>
<circle cx="44" cy="22" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Construction line at an angle</title>
<path d="M10.26 48.66L58.26 48.66" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.65 33.23A24 24 0 0 1 34.26 48.66" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10.26 48.66L54.26 11.66" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="10.26" cy="48.66" r="5.8" fill="#ffffff"/>
<circle cx="10.26" cy="48.66" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Angle bisector</title>
<path d="M13.92 12.18L9.92 52.17L54.92 44.17" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M12.11 30.29A22 22 0 0 1 31.58 48.33" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.92 52.17L47.3 11.83" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Horizontal construction line</title>
<path d="M8 32L56 32" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
<circle cx="32" cy="32" r="5.8" fill="#ffffff"/>
<circle cx="32" cy="32" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Parallel construction line</title>
<path d="M9 37L41 9" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23 55L55 27" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Perpendicular construction line</title>
<path d="M8 44L56 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 8L32 56" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 34L42 34L42 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 490 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Tangent at a curve point</title>
<circle cx="32" cy="34.26" r="17" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8 17.26L56 17.26" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="17.26" r="5.8" fill="#ffffff"/>
<circle cx="32" cy="17.26" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 480 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Tangent through an external point</title>
<circle cx="21.74" cy="38.77" r="14" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M53.6 8.23L28.55 55.9" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="51.74" cy="11.77" r="5.8" fill="#ffffff"/>
<circle cx="51.74" cy="11.77" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Common tangent to two curves</title>
<circle cx="20" cy="33.59" r="12" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="46" cy="33.59" r="8" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 19.27L58 27.36" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Vertical construction line</title>
<path d="M32 8L32 56" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
<circle cx="32" cy="32" r="5.8" fill="#ffffff"/>
<circle cx="32" cy="32" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Equation curve</title>
<path d="M12 8L12 56M6 32L58 32" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M12 32C15.42 24.83 18.72 18 22.5 18C26.28 18 29.58 24.83 33 32C36.42 39.17 39.72 46 43.5 46C47.28 46 50.58 39.17 54 32" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="12" cy="32" r="5.8" fill="#ffffff"/>
<circle cx="12" cy="32" r="3.4" fill="#236ead"/>
<circle cx="54" cy="32" r="5.8" fill="#ffffff"/>
<circle cx="54" cy="32" r="3.4" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 666 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Feature group</title>
<path d="M28.5 11L50.5 11A4.5 4.5 0 0 1 55 15.5L55 28.5A4.5 4.5 0 0 1 50.5 33L28.5 33A4.5 4.5 0 0 1 24 28.5L24 15.5A4.5 4.5 0 0 1 28.5 11Z" fill="#8e9faf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 20L43 20A4.5 4.5 0 0 1 47.5 24.5L47.5 37.5A4.5 4.5 0 0 1 43 42L21 42A4.5 4.5 0 0 1 16.5 37.5L16.5 24.5A4.5 4.5 0 0 1 21 20Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13.5 29L35.5 29A4.5 4.5 0 0 1 40 33.5L40 46.5A4.5 4.5 0 0 1 35.5 51L13.5 51A4.5 4.5 0 0 1 9 46.5L9 33.5A4.5 4.5 0 0 1 13.5 29Z" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M16 40L33 40" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 947 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Mesh body</title>
<path d="M8.62 18.5L32 32L32 59L8.62 45.5Z" fill="#328bd3"/>
<path d="M55.38 18.5L32 32L32 59L55.38 45.5Z" fill="#236ead"/>
<path d="M32 5L55.38 18.5L32 32L8.62 18.5Z" fill="#b9ddf8"/>
<path d="M8.62 18.5L55.38 18.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8.62 18.5L32 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M55.38 18.5L32 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 5L55.38 18.5L55.38 45.5L32 59L8.62 45.5L8.62 18.5Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8.62 18.5L32 32L55.38 18.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Plane through axis and point</title>
<path d="M26 15L56 32.32L38 42.71L8 25.39Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0.1 5"/>
<path d="M15 18.4L53 40.34" fill="none" stroke="currentColor" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="22" cy="29" r="3.1" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Print Face</title>
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="#b8c3cf"/>
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="#328bd3"/>
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="#e2e8ef"/>
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="#b8c3cf"/>
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="#8e9faf"/>
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="#e2e8ef"/>
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M50.45 46.07L38.27 33.88" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M59.5 43.05L50.45 46.07" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M24.17 34.2L28.31 34.2" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M35.31 34.2L27.31 38.7L27.31 29.7Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>Play History</title><path d="M11.22 20L32 32L32 56L11.22 44Z" fill="#b8c3cf"/><path d="M52.78 20L32 32L32 56L52.78 44Z" fill="#8e9faf"/><path d="M32 8L52.78 20L32 32L11.22 20Z" fill="#e2e8ef"/><path d="M32 8L52.78 20L52.78 44L32 56L11.22 44L11.22 20Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M11.22 20L32 32L52.78 20" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M32 32L32 56" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M35 34L56 46L35 58Z" fill="#ffffff" stroke="#ffffff" stroke-width="4.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M35 34L56 46L35 58Z" fill="#236ead" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 935 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>Stop History</title><path d="M11.22 20L32 32L32 56L11.22 44Z" fill="#b8c3cf"/><path d="M52.78 20L32 32L32 56L52.78 44Z" fill="#8e9faf"/><path d="M32 8L52.78 20L32 32L11.22 20Z" fill="#e2e8ef"/><path d="M32 8L52.78 20L52.78 44L32 56L11.22 44L11.22 20Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M11.22 20L32 32L52.78 20" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M32 32L32 56" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M36 35L53 35A2 2 0 0 1 55 37L55 54A2 2 0 0 1 53 56L36 56A2 2 0 0 1 34 54L34 37A2 2 0 0 1 36 35Z" fill="#ffffff" stroke="#ffffff" stroke-width="4.55" stroke-linejoin="round" stroke-linecap="round"/><path d="M36 35L53 35A2 2 0 0 1 55 37L55 54A2 2 0 0 1 53 56L36 56A2 2 0 0 1 34 54L34 37A2 2 0 0 1 36 35Z" fill="#236ead" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+11
View File
@@ -15,6 +15,17 @@ pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col:
}
"back" => tri(-1.0, 0.0),
"forward" => tri(1.0, 0.0),
// Play History: larger than the step arrows so it doesn't read as Forward, and two bars
// while it plays.
"play" => {
let pts = vec![pos2(c.x - 4.5, c.y - 7.0), pos2(c.x + 6.5, c.y), pos2(c.x - 4.5, c.y + 7.0)];
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
}
"pause" => {
for dx in [-3.0, 3.0] {
p.rect_filled(Rect::from_center_size(pos2(c.x + dx, c.y), vec2(3.0, 12.0)), 0.5, col);
}
}
_ => {
tri(1.0, -1.0);
bar(5.0);
+1235 -34
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2751 -1
View File
File diff suppressed because it is too large Load Diff
+1884 -6
View File
File diff suppressed because it is too large Load Diff
+137 -2
View File
@@ -1,40 +1,111 @@
{
"A Single hole snaps to the middle of its face and to the centres of its round edges": "A Single hole snaps to the middle of its face and to the centres of its round edges",
"Base Center": "Base Centre",
"Center X": "Centre X",
"Center Y": "Centre Y",
"Center of mass": "Centre of mass",
"Centre to centre": "Centre to centre",
"Circle center": "Circle centre",
"Colour": "Colour",
"Copy all licences": "Copy all licences",
"Customize Toolbar": "Customise Toolbar",
"Customize…": "Customise…",
"Face center": "Face centre",
"Favorites": "Favourites",
"Favourite": "Favourite",
"Mass Center": "Mass Centre",
"Modeled": "Modelled",
"Select the centre the pattern turns around first": "Select the centre the pattern turns around first",
"Snap to face and edge centres": "Snap to face and edge centres",
"Specialise": "Specialise",
"cmd.appearance_favorite": "Favourite Look",
"cmd.component_cycle_color": "Cycle Component Colour",
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
"cmd.inspect_center_of_mass": "Centre of Mass",
"cmd.inspect_colored_components": "Enable Component Colours",
"cmd.inspect_colored_components_disable": "Disable Component Colours",
"cmd.parameters_favorite": "Favourite Parameter",
"cmd.render_aging_describe": "Layered Surface Aging",
"cmd.render_effects_describe": "Lens and Creative Effects",
"cmd.sketch.construction_line": "Construction Line",
"cmd.sketch.construction_line.angle": "Construction Line at Angle",
"cmd.sketch.construction_line.bisector": "Angle Bisector",
"cmd.sketch.construction_line.horizontal": "Horizontal Construction Line",
"cmd.sketch.construction_line.parallel": "Parallel Construction Line",
"cmd.sketch.construction_line.perpendicular": "Perpendicular Construction Line",
"cmd.sketch.construction_line.tangent_at": "Tangent at Curve Point",
"cmd.sketch.construction_line.tangent_external": "Tangent through External Point",
"cmd.sketch.construction_line.tangent_two": "Common Tangent",
"cmd.sketch.construction_line.vertical": "Vertical Construction Line",
"cmd.sketch_arc_center_point": "Centre Point Arc",
"cmd.sketch_circle_center": "Centre Diameter Circle",
"cmd.sketch_rectangle_center": "Centre Rectangle",
"cmd.sketch_slot_arc_center": "Centre Point Arc Slot",
"cmd.sketch_slot_center_point": "Centre Point Slot",
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
"color.copy": "Click to copy colour values",
"color.edit": "Click to edit colour",
"color.gamma": "Showing colour values in 0-255 gamma space",
"color.linear": "Showing colour values in 0-1 linear space",
"color.recent": "Recent colours",
"color.recent_help": "Colours you pick in any colour field appear here",
"color.saturation": "Saturation (per cent)",
"color.selected": "Selected colour",
"color.step_down": "Step down",
"color.step_up": "Step up",
"color.value": "Value, the brightness (per cent)",
"dxf.cancelled": "DXF export cancelled",
"dxf.cancelling": "Cancelling DXF export",
"help.apache": "Apache licence 2.0",
"help.appearance.assign": "Gives the selected bodies, faces or components a colour or a material look, without changing their weight. Use it to tell parts apart or to preview a finish.",
"help.face.delete": "Removes the selected faces and lets the neighbouring faces close the gap, or leaves the gap open as a surface. Use it to take off a rounded edge, a hole or a boss that is in the way.",
"help.font_details": "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans (SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.",
"help.licences": "Licences",
"help.material.assign": "Sets what the selected bodies are made of, such as steel, aluminium or ABS. Use it so that the mass and weight match the real part.",
"help.mit": "MIT licence",
"help.solid.draft": "Tilts the selected faces by the angle you set, measured from a neutral plane. Use it on moulded or cast parts so that they slide out of the mould.",
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
"help.solid.split_body": "Cuts a body into separate bodies with a plane, the face of another body or a sketch line. Use it to make parts that are printed, machined or coloured on their own.",
"help.solid.split_face": "Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
"help.third_party": "Third-party licences",
"home.new_mm_hint": "Millimetres · opens in Design",
"navbar.custom.customise": "Customise navigation bar…",
"navbar.custom.position_centre": "Centre",
"navbar.description.separator": "A thin line that groups neighbouring slots.",
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"prefs.bottom_centre": "Bottom centre",
"prefs.logo_theme": "Theme's colours",
"prefs.max_file_size": "Largest file to open",
"prefs.max_file_size_design": "Design files",
"prefs.max_file_size_flag": "This run started with --max-file-size {size}, which applies until SolidCraft closes. These settings apply when it starts without it.",
"prefs.max_file_size_note": "Defaults: 512 MB and 256 MB; at most 16 GB. Opening a file takes several times its size in memory. A design that keeps a large import may need a higher design file limit to save and reopen.",
"prefs.max_file_size_step": "STEP and import files",
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
"prefs.megabytes": "MB",
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
"prefs.sketch_conflict_colour": "Conflicting geometry in the conflict colour",
"prefs.sketch_conflict_colour_note": "While a sketch's constraints conflict, the curves and points a failing constraint holds are drawn in the conflict colour too, not only its glyph or dimension. Off: they keep their usual colours.",
"prefs.sketch_construction_lines": "Extended construction line tools",
"prefs.sketch_recent_snap": "Auto snap to recent centres and midpoints",
"prefs.sketch_recent_snap_note": "While drawing, snap the cursor where the guides of the two most recent circle centres or straight-edge midpoints cross. Off: those guides stay visual and the cursor does not move.",
"prefs.sketch_state_icons": "Sketch icons in the colour of their state",
"prefs.sketch_state_icons_note": "In History and the browser, a sketch's icon takes the colour of its state (under constrained, fully constrained or conflicting) and its tooltip names the state. Off: every sketch has the same icon.",
"prefs.sketch_tracking_guides": "Centre and midpoint alignment guides",
"prefs.sketch_tracking_guides_note": "Hover over a circle centre or straight-edge midpoint, then move horizontally or vertically to show a temporary dotted guide.",
"prefs.status_bar_note": "A thin bar along the window's bottom edge shows what is being edited, the open sketch's state in its colour and the design's units; a click on the units changes them. The sketch's state then no longer shows in the view's corner.",
"prefs.structure.appearance_note": "Colours, text and accessibility.",
"prefs.structure.tools_note": "How modelling tools behave.",
"prefs.theme_follow": "Follow a theme file",
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
"prefs.theme_follow_off": "Off",
"prefs.theme_follow_stop": "Stop following",
"prefs.theme_option_opacity_help": "Adjust floating panel opacity. A readability floor protects text over viewport colours.",
"prefs.theme_option_tab_colours": "Tab colours",
"prefs.theme_option_tab_colours_help": "Use this theme’s panel and viewport colours for each ribbon tab.",
"prefs.theme_option_tab_fade_ms_help": "Fade panel and viewport colours when switching tabs. Zero switches instantly.",
"prefs.trackpad_zoom_modifier.help": "Hold only this key to zoom at the pointer with vertical scroll. Off keeps the existing scroll behaviour.",
"prefs.view": "View behaviour",
"prefs.viewport_background_colour": "Colour",
"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.",
@@ -62,6 +133,9 @@
"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.appearance_editor.base_colour": "Base colour",
"render.backdrop_first": "Start / centre colour",
"render.backdrop_second": "End / edge colour",
"render.backdrop_x": "Centre X",
"render.backdrop_y": "Centre Y",
"render.background_camera_section": "Camera background · PNG presentation",
@@ -69,20 +143,38 @@
"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_not_light": "Background colour is presentation, not an additional lighting source.",
"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.base_color": "Base colour · sRGB",
"render.bloom_help": "Creative output-only glow from positive linear highlights, spread with three normalised box passes. Radius is in output pixels. Added in linear display light before sRGB; not a lens, volumetric effect or convergence improvement. The raw EXR is unchanged.",
"render.chromatic_offset": "Chromatic edge offset",
"render.cpu_area_editor_help": "Full side vectors in millimeters; U cross V points towards the emitting side. This differs from GPU half-edge vectors.",
"render.cpu_light_help": "Up to eight directional lights. An empty custom list turns them off; selecting a preset restores its lights. Colours are linear RGB and intensity is relative, not watts.",
"render.cpu_linear_color": "Linear colour (R, G, B)",
"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.env_colour": "Colour",
"render.environment.radial_only": "Centre and radius apply to Radial mode.",
"render.error_finish_gesture": "finish the modelling gesture before previewing",
"render.error_finish_operation": "finish the current modelling operation before entering Render",
"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.frame_centre_mark": "Centre mark",
"render.hdr_primaries_declared": "The source declares its colour primaries.",
"render.hdr_primaries_srgb": "Colour primaries: sRGB/D65",
"render.hdr_primaries_undeclared": "The source does not declare primaries; review the renderer colour requirements.",
"render.image_cancelled": "Render cancelled",
"render.image_cancelling": "Cancelling",
"render.image_scene_stale": "Finish the modelling gesture or refresh the scene before checking or starting an image.",
"render.inspector_colour": "Colour",
"render.layer_down": "Move down",
"render.layer_enabled": "Layer enabled",
"render.layer_remove": "Remove layer",
@@ -90,6 +182,18 @@
"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.library_center": "Rig centre",
"render.library_favorite": "Add favourite",
"render.library_favorites": "Favourites",
"render.library_inspect_only": "Select to inspect; nothing is applied automatically. Favourites and recent history belong to this open document session.",
"render.library_rig_help": "Fit once at an explicit centre and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
"render.library_source_preview_help": "Authored base-colour source map. This is not a rendered or measured material preview.",
"render.library_unfavorite": "Remove favourite",
"render.light_behaviour": "BEHAVIOUR",
"render.light_preview_help": "Live view is unshadowed; Area lights use a centre-sample approximation. Render uses the complete light model.",
"render.light_product_centre": "Product centre",
"render.light_temperature": "Colour temperature",
"render.lighting_colour": "Colour",
"render.look_add": "Add colour look",
"render.look_bleach": "Bleach-bypass-inspired",
"render.look_cool": "Cool",
@@ -101,6 +205,11 @@
"render.look_teal_orange": "Teal / orange",
"render.look_title": "Creative look stack",
"render.look_warm": "Warm",
"render.material_color_map": "Base colour image (sRGB)",
"render.material_color_replace": "Base map replaces body colour",
"render.material_map_required": "Choose a colour image or a normal image before using maps.",
"render.material_maps": "Planar colour and normal maps",
"render.material_maps_help": "PNG/JPEG up to 1024×1024; 16 MiB encoded total. U and V are orthogonal world-mm repeat vectors (maximum 128 repeats). Colour is sRGB; normal data uses OpenGL +Y. Files are captured at Check/Start.",
"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",
@@ -108,22 +217,48 @@
"render.radial_distortion": "Radial distortion",
"render.ribbon_aging": "Aging",
"render.ribbon_effects": "Effects",
"render.ribbon_studios_help": "Browse implemented studio compositions and review their replacements before applying. Concept-only scenes remain labelled.",
"render.sharpening": "Detail sharpening",
"render.soft_focus": "Soft focus",
"render.stage_panels.centre": "Centre",
"render.stage_panels.source_note": "Authored base-colour source map. Not a measured material.",
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
"render.studio_center": "Centre X, Y, Z (mm)",
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
"sketch_text.align_center": "Centre",
"ui.3d_mouse_kind_cap": "Return to Centre",
"ui.3d_mouse_looks_like_cap": "The reading stays put while you hold it, the way a Return to Centre device does.",
"ui.3d_mouse_orbit_tip": "Traced: the 3D mouse turns the view around the part of the model in the middle of the view when the turn begins, marked by a faint ball. Classic: around the view's centre",
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
"ui.3d_mouse_switch_cap": "Calibrate as Return to Centre",
"ui.agent_chat_minimize": "Minimise to a strip",
"ui.ai_mode_edits_note": "Modelling commands run without asking; saving, opening, exporting and other file or destructive actions still ask.",
"ui.center": "Centre",
"ui.center_arc_follows_cursor": "Centre point arc follows the cursor's direction",
"ui.center_arc_follows_cursor_tip": "A centre point arc runs clockwise or counter-clockwise the way the cursor goes round its centre. Off: always counter-clockwise from the start.",
"ui.centerline": "Centreline",
"ui.colour_vision": "Colour vision",
"ui.colour_vision_note": "Changes the colours that carry a meaning: sketch states, axes, bends, analyses and icons. If you use Windows colour filters, keep this Off.",
"ui.colour_vision_off_tip": "The theme's own colours",
"ui.construction.ribbon_label": "X-Lines",
"ui.customizeToolbar": "Customise Toolbar…",
"ui.default_modelling_orientation": "Default modelling orientation",
"ui.design_properties.license": "Licence",
"ui.design_properties.organization": "Organisation",
"ui.details_curvature_caption": "Faces are coloured by their local radius of curvature.",
"ui.details_draft_caption": "Faces are coloured by their angle to the pull direction.",
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
"ui.instances": "Instances",
"ui.layout_compact": "Compact",
"ui.layout_floating": "Floating",
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
"ui.point_cloud_color_by": "Colour By",
"ui.point_cloud_file_colors": "File Colours",
"ui.point_cloud_no_file_colors": "The file has no colours",
"ui.point_cloud_single_color": "Single Colour",
"ui.properties.inertia_note": "About the centre of mass, in world axes (g·{unit}²)",
"ui.ribbon_labels": "Show text under ribbon icons",
"ui.view_centre": "View centre",
"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."
+1261 -60
View File
File diff suppressed because it is too large Load Diff
+1989 -41
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1209 -8
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1209 -8
View File
File diff suppressed because it is too large Load Diff
+791 -5
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1303 -1
View File
File diff suppressed because it is too large Load Diff
+1193 -1
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -606,6 +606,8 @@
"cmd.appearance_library": "Biblioteca de aparências",
"cmd.appearance_list": "Listar aparências",
"cmd.assembly_bom": "Lista de materiais",
"cmd.assembly_mass_properties": "Propriedades de massa",
"cmd.body.print_face": "Face de impressão",
"cmd.body_rename": "Renomear corpo",
"cmd.browser_group": "Novo grupo",
"cmd.browser_move": "Mover itens do navegador",
@@ -815,6 +817,16 @@
"cmd.sheet_refold": "Redobrar",
"cmd.sheet_rules": "Listar regras de chapa metálica",
"cmd.sheet_unfold": "Desdobrar",
"cmd.sketch.construction_line": "Reta de construção",
"cmd.sketch.construction_line.angle": "Reta de construção em ângulo",
"cmd.sketch.construction_line.bisector": "Bissetriz",
"cmd.sketch.construction_line.horizontal": "Reta de construção horizontal",
"cmd.sketch.construction_line.parallel": "Reta de construção paralela",
"cmd.sketch.construction_line.perpendicular": "Reta de construção perpendicular",
"cmd.sketch.construction_line.tangent_at": "Tangente em um ponto da curva",
"cmd.sketch.construction_line.tangent_external": "Tangente por um ponto externo",
"cmd.sketch.construction_line.tangent_two": "Tangente comum",
"cmd.sketch.construction_line.vertical": "Reta de construção vertical",
"cmd.sketch_arc_center_point": "Arco por ponto central",
"cmd.sketch_arc_tangent": "Arco tangente",
"cmd.sketch_arc_three_point": "Arco por 3 pontos",
@@ -1207,6 +1219,8 @@
"prefs.axis": "Eixo",
"prefs.bottom": "Embaixo",
"prefs.browser": "Navegador",
"prefs.browser_components_open": "Componentes começam abertos",
"prefs.browser_components_open_note": "Cada componente no navegador começa aberto, como antes, em vez de fechado até que você o abra.",
"prefs.built_in": "Integrado",
"prefs.button_left": "Arrastar com o esquerdo",
"prefs.button_middle": "Arrastar com o meio",
@@ -1282,6 +1296,8 @@
"prefs.orientation": "Orientação",
"prefs.overall_speed": "Velocidade geral",
"prefs.preview_choices": "Prévia das suas escolhas",
"prefs.print_face_highlight": "Destaque da face de impressão",
"prefs.print_face_highlight_tip": "Intensidade da cor e do contorno da face de impressão de um corpo na vista. 0 % não mostra destaque.",
"prefs.quality_default": "padrão",
"prefs.quality_fastest": "mais rápido",
"prefs.quality_slowest": "mais lento",
@@ -1561,6 +1577,8 @@
"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_use_direct": "Usar driver direto",
"ui.3d_mouse_use_direct_tip": "Ler o mouse 3D diretamente (hidraw) em vez de pelo spacenavd.",
"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.",
@@ -1830,6 +1848,7 @@
"ui.circle": "Círculo",
"ui.circular_pattern": "Padrão circular",
"ui.clear": "Limpar",
"ui.clear_print_face": "Limpar face de impressão",
"ui.clear_recent": "Limpar recentes",
"ui.clear_the_selection": "Limpar a seleção",
"ui.click_selects_whole_bodies": "O clique seleciona corpos inteiros",
@@ -1864,6 +1883,7 @@
"ui.constant": "Constante",
"ui.constraints_conflict": "Restrições em conflito",
"ui.construction": "Construção",
"ui.construction.ribbon_label": "Linhas aux.",
"ui.context_menu": "Menu de contexto",
"ui.continuity_g0": "G0 vinco",
"ui.continuity_g1": "G1 tangente",
@@ -2074,6 +2094,7 @@
"ui.infinite": "Infinita",
"ui.insert": "Inserir",
"ui.inspect": "Inspecionar",
"ui.instances": "Instâncias",
"ui.intersect": "Interseção",
"ui.isolate": "Isolar",
"ui.join": "Unir",
@@ -2161,6 +2182,7 @@
"ui.pan_zoom_orbit": "Deslocar, zoom, orbitar",
"ui.parallel": "Paralelo",
"ui.parameters": "Parâmetros",
"ui.part_size_needs_a_part": "Selecione uma peça padrão inserida (um parafuso, uma porca, uma arruela ou um pino) para alterar o tamanho. Para aumentar ou diminuir um corpo, use Escala.",
"ui.part_size_shared": "Reconstrói a peça, substituindo quaisquer recursos adicionados. Todas as ocorrências deste componente são atualizadas.",
"ui.paste": "Colar",
"ui.path": "Caminho",
@@ -2186,6 +2208,11 @@
"ui.polygon": "Polígono",
"ui.preferences": "Preferências",
"ui.press_a_key": "pressione uma tecla…",
"ui.print_face_body_not_sent": "O corpo dessa face não está sendo enviado: adicione-o primeiro a Corpos",
"ui.print_face_cleared": "{body}: face de impressão removida",
"ui.print_face_input": "Face de impressão",
"ui.print_face_pick": "Selecione uma face plana de um corpo sólido para torná-la a face de impressão",
"ui.print_face_set": "{body}: face de impressão definida; Enviar ao fatiador apoia o corpo sobre ela",
"ui.profile": "Perfil",
"ui.profile_plane": "Plano do perfil",
"ui.profiles": "Perfis",
@@ -2243,6 +2270,7 @@
"ui.select": "Selecionar",
"ui.selectOther": "Selecionar outro…",
"ui.selectTangentChain": "Selecionar cadeia tangente",
"ui.select_all": "Selecionar tudo",
"ui.selection": "Seleção",
"ui.selection_modifiers": "Modificadores de seleção",
"ui.selection_shift_add_only": "Shift+clique adiciona sem remover",
@@ -2304,6 +2332,10 @@
"ui.symmetric": "Simétrico",
"ui.symmetry": "Simetria",
"ui.system": "Sistema",
"ui.tab_assemble": "Montar",
"ui.tab_manage": "Gerenciar",
"ui.tab_sheet_metal": "Chapa Metálica",
"ui.tab_view": "Vista",
"ui.tangent": "Tangente",
"ui.taper_angle": "Ângulo de inclinação",
"ui.tapped": "Roscado",
@@ -2371,7 +2403,9 @@
"update.button": "Atualizar",
"update.button_version": "Atualizar para {version}",
"update.check_automatically": "Verificar atualizações automaticamente",
"update.check_for_updates": "Verificar atualizações",
"update.check_now": "Verificar agora",
"update.check_unavailable": "A verificação de atualizações não está disponível para esta instalação.",
"update.checking": "Verificando…",
"update.copy": "Copiar",
"update.dmg_opened": "A imagem de disco está aberta e verificada: arraste o SolidCraft para Aplicativos, substituindo a cópia antiga, e reinicie o SolidCraft.",
File diff suppressed because it is too large Load Diff
+1100 -1
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1209 -8
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1209 -8
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ pub const DEFAULT_MATERIAL: &str = "Default";
pub const DEFAULT_DENSITY: f64 = 1.29;
/// Physical materials: name and density (g/cm³).
pub const MATERIALS: [(&str, f64); 14] = [
pub const MATERIALS: &[(&str, f64)] = &[
(DEFAULT_MATERIAL, DEFAULT_DENSITY),
("Steel", 7.85),
("Stainless Steel", 8.0),
+285 -28
View File
@@ -18,6 +18,9 @@ struct U {
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
tint: vec4<f32>, // xyz added to a whole hovered body's colour before lighting
sel: vec4<f32>, // xyz a whole selected body's colour before lighting
product_lights_meta: vec4<f32>, // explicit preview flag and bounded count
product_lights: array<vec4<f32>,84>, // position/kind, radiance, axis/cone, attenuation
sky: vec4<f32>, // xyz = world up; w = 1: shade in linear light with a sky fill (#7974)
};
fn reversed() -> bool {
@@ -32,6 +35,7 @@ fn near_z(c: vec4<f32>) -> f32 {
@group(1) @binding(2) var acc_t: texture_2d<f32>;
@group(2) @binding(0) var ao_depth: texture_2d<u32>;
@group(2) @binding(1) var ao_map: texture_2d<f32>;
fn clipped(p: vec3<f32>) -> bool {
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
@@ -59,7 +63,7 @@ fn out_color(c: vec4<f32>) -> vec4<f32> {
}
struct TOut {
@builtin(position) pos: vec4<f32>,
@invariant @builtin(position) pos: vec4<f32>,
@location(0) n: vec3<f32>,
@location(1) c: vec4<f32>,
@location(2) wp: vec3<f32>,
@@ -104,7 +108,7 @@ fn vs_tri_inst(
/// stores every vertex's outputs: more of them cost more than the triangles' shared vertices
/// save).
struct FOut {
@builtin(position) pos: vec4<f32>,
@invariant @builtin(position) pos: vec4<f32>,
@location(0) rp: vec3<f32>,
@location(1) c: vec4<f32>,
};
@@ -206,6 +210,12 @@ fn fs_ao_depth_flat(i: FOut, @builtin(front_facing) front: bool) -> AoDepth {
return opaque_depth(t, front);
}
@fragment
fn fs_ao_term_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let t = flat_fragment(i, front);
return ao_term(t, front);
}
/// Opaque model surfaces: cut by the section plane/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
@fragment
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
@@ -241,6 +251,44 @@ fn pushed(z: f32) -> f32 {
return clamp(z - select(-push, push, reversed()), 0.0, 1.0);
}
/// A far wall seen through the cut where another body's face lying on it was drawn in front of
/// it (a shaft in its bore, #9407): its cap colour again, at its depth pulled toward the eye by
/// `touch_depth`, so it wins over that face, which is faceted another way.
@fragment
fn fs_solid_touch(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
var o: CutOut;
// Derivatives (in `touch_depth`) first, before `solid` may discard.
o.depth = touch_depth(i.wp, i.pos.z);
o.c = solid(i, front);
return o;
}
@fragment
fn fs_solid_touch_flat(i: FOut, @builtin(front_facing) front: bool) -> CutOut {
let t = flat_fragment(i, front);
var o: CutOut;
o.depth = touch_depth(t.wp, t.pos.z);
o.c = solid(t, front);
return o;
}
/// The depth of `wp` (depth `z`) pulled toward the eye by what may part two faceted copies of
/// one surface: 3 pixels' width along the view ray (display refinement keeps a facet's sag about
/// a pixel) and 2 pixels of the face's own depth slope (more where it turns from the eye).
fn touch_depth(wp: vec3<f32>, z: f32) -> f32 {
let px = min(length(dpdx(wp)), length(dpdy(wp)));
let slope = max(abs(dpdx(z)), abs(dpdy(z)));
var dir = -u.back.xyz;
if (u.eye.w > 0.5) { dir = wp - u.eye.xyz; }
let len = length(dir);
if (len < 1e-12) { return z; }
let c = u.vp * vec4<f32>(wp - dir * (3.0 * px / len), 1.0);
if (c.w <= 0.0) { return z; }
let pulled = c.z / c.w;
if (reversed()) { return clamp(max(pulled, z) + 2.0 * slope, 0.0, 1.0); }
return clamp(min(pulled, z) - 2.0 * slope, 0.0, 1.0);
}
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
/// never behind the wall itself (`z`).
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
@@ -305,11 +353,89 @@ fn opaque_depth(i: TOut, front: bool) -> AoDepth {
return AoDepth(depth, bitcast<u32>(depth));
}
/// The term for an opaque surface's fragment, on the prepass's depth. A section's cut face has none.
fn ao_term(i: TOut, front: bool) -> vec4<f32> {
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
if (!front && length(u.clip.xyz) > 0.5) { return vec4<f32>(1.0); }
return vec4<f32>(occlusion_term(i));
}
@fragment
fn fs_ao_term(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
return ao_term(i, front);
}
@vertex
fn vs_ao_blur(@builtin(vertex_index) v: u32) -> @builtin(position) vec4<f32> {
let p = vec2<f32>(f32((v << 1u) & 2u), f32(v & 2u));
return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}
/// The depth map at `p`, reading 0 near and 1 far whichever way the buffer runs.
fn map_depth(p: vec2<i32>) -> f32 {
let at = clamp(p, vec2<i32>(0), vec2<i32>(textureDimensions(ao_depth)) - vec2<i32>(1));
let d = bitcast<f32>(textureLoad(ao_depth, at, 0).x);
return select(d, 1.0 - d, reversed());
}
/// The raw term (`ao_map` in this pass) averaged over a 4 x 4 window, which holds every turn of
/// [`ao_turn`]'s pattern once. A neighbour counts when it lies on this pixel's plane, the slope
/// taken from the nearer side across each axis, so no surface's shade crosses an edge.
@fragment
fn fs_ao_blur(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let c = vec2<i32>(pos.xy);
let d = map_depth(c);
if (d >= 1.0) { return vec4<f32>(1.0); }
let left = d - map_depth(c + vec2<i32>(-1, 0));
let right = map_depth(c + vec2<i32>(1, 0)) - d;
let above = d - map_depth(c + vec2<i32>(0, -1));
let below = map_depth(c + vec2<i32>(0, 1)) - d;
let dx = select(right, left, abs(left) < abs(right));
let dy = select(below, above, abs(above) < abs(below));
let tolerance = 1.5 * (abs(dx) + abs(dy)) + 1e-6;
let last = vec2<i32>(textureDimensions(ao_map)) - vec2<i32>(1);
var sum = 0.0;
var weight = 0.0;
for (var y = -2; y < 2; y = y + 1) {
for (var x = -2; x < 2; x = x + 1) {
let p = c + vec2<i32>(x, y);
let s = map_depth(p);
if (s < 1.0 && abs(s - (d + dx * f32(x) + dy * f32(y))) <= tolerance) {
sum = sum + textureLoad(ao_map, clamp(p, vec2<i32>(0), last), 0).x;
weight = weight + 1.0;
}
}
}
return vec4<f32>(sum / max(weight, 1.0));
}
/// A pixel's turn of the sample pattern: every 4 x 4 window holds each of 16 turns once.
fn ao_turn(pos: vec2<f32>) -> f32 {
let x = u32(pos.x) & 3u;
let y = u32(pos.y) & 3u;
// WGSL won't mix shifts and `|` without parentheses; naga takes it, browsers refuse it (#11080).
let v = (((x ^ y) & 1u) << 3u) | ((y & 1u) << 2u) | (((x ^ y) >> 1u) << 1u) | (y >> 1u);
return (f32(v) + 0.5) / 16.0;
}
/// The lit pass's occlusion: the blurred term where this point falls in the map, whatever the
/// window's viewport offset and supersampling.
fn occlusion(i: TOut) -> f32 {
let size = vec2<i32>(textureDimensions(ao_map));
// The maps are 1 × 1 stand-ins until a frame draws into them (the product preview draws no
// prepass, and AO off releases them): they tell nothing, so leave the surface unoccluded.
if (u.ao.x <= 0.0 || length(i.n) < 0.5 || size.x < 2 || size.y < 2) { return 1.0; }
let q = u.vp * vec4<f32>(i.wp, 1.0);
if (q.w <= 0.0) { return 1.0; }
let uv = vec2<f32>(q.x / q.w * 0.5 + 0.5, 0.5 - q.y / q.w * 0.5);
return textureLoad(ao_map, clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1)), 0).x;
}
// Cosine-weighted hemisphere samples, the pattern turned by a different angle at each pixel.
// Projecting the sample and a point one radius toward the eye supplies a depth range in both
// orthographic and perspective views; distant foreground geometry cannot cast a dark halo onto
// a background surface.
fn occlusion(i: TOut) -> f32 {
fn occlusion_term(i: TOut) -> f32 {
let radius = u.ao.x;
if (radius <= 0.0 || length(i.n) < 0.5) { return 1.0; }
var n = normalize(i.n);
@@ -323,10 +449,9 @@ fn occlusion(i: TOut) -> f32 {
if (size.x < 2 || size.y < 2) { return 1.0; }
// The same offsets at every pixel made each sample darken its own shifted copy of a nearby
// part's outline: faint ghosts of the part where the radius is large on screen (#4167),
// stepped bands along contacts where it is small (#3407). Turned by interleaved gradient
// noise (Jimenez 2014), neighbouring pixels sample different points and the copies break up
// into an even grain.
let turn = fract(52.9829189 * fract(dot(i.pos.xy, vec2<f32>(0.06711056, 0.00583715))));
// stepped bands along contacts where it is small (#3407). Turned, neighbouring pixels sample
// different points, the copies break up into grain, and the blur takes the grain out (#9327).
let turn = ao_turn(i.pos.xy);
let t = tangent * cos(turn * 6.28318531) + bitangent * sin(turn * 6.28318531);
let b = cross(n, t);
// One comparison for both depth directions: depths read as 0 near / 1 far (mirrored when
@@ -383,8 +508,13 @@ fn analysis(i: TOut, k: f32) -> vec4<f32> {
let r = reflect(-v, n);
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
let c = select(0.08, 0.95, s > 0.0);
let x = dot(r, normalize(side));
let s = sin(x * u.ana.y * 3.14159265);
// A face whose normal lies in the upright plane through the view (every horizontal
// face) reflects straight ahead: its sideways part is zero, on the edge between a black
// and a white stripe, and rounding picked one or the other from frame to frame while
// the view turned (#2153). Straight ahead is white.
let c = select(0.08, 0.95, s > 0.0 || abs(x) < 1e-4);
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
}
if (u.ana.x > 4.5) {
@@ -471,10 +601,65 @@ fn shade(i: TOut) -> vec4<f32> {
return lit(i, 1.0);
}
fn product_light_unit(v: vec3<f32>) -> vec3<f32> {
let scale=max(max(abs(v.x),abs(v.y)),abs(v.z));
if scale == 0.0 { return vec3<f32>(0.0,0.0,1.0); }
return normalize(v/scale);
}
// Direction and incident radiance for an unshadowed analytic preview sample.
fn product_light_sample(k: u32, p: vec3<f32>) -> mat2x3<f32> {
let source=u.product_lights[k*4u];
let colour=u.product_lights[k*4u+1u];
let axis=u.product_lights[k*4u+2u];
let falloff=u.product_lights[k*4u+3u];
var direction=product_light_unit(source.xyz);
var weight=1.0;
if source.w > 0.5 {
let delta=source.xyz-p;
let distance=max(length(delta),falloff.y);
direction=product_light_unit(delta);
weight=falloff.x/max(distance*distance,0.000001);
if source.w == 2.0 {
let cosine=dot(product_light_unit(axis.xyz),-direction);
var cone=select(0.0,1.0,cosine>=colour.w);
if axis.w-colour.w>0.000001 {cone=smoothstep(colour.w,axis.w,cosine);}
weight *= cone;
}
if source.w == 3.0 {weight *= max(dot(product_light_unit(axis.xyz),-direction),0.0);}
}
return mat2x3<f32>(direction,colour.xyz*weight);
}
fn lit(i: TOut, visibility: f32) -> vec4<f32> {
return lit_color(i, visibility, i.c);
}
/// sRGB to linear light, exact (the straight segment too, which `to_linear` leaves out).
fn srgb_decode(c: vec3<f32>) -> vec3<f32> {
return select(c / 12.92, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
}
fn srgb_encode(c: vec3<f32>) -> vec3<f32> {
let l = clamp(c, vec3<f32>(0.0), vec3<f32>(1.0));
return select(l * 12.92, 1.055 * pow(l, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055), l > vec3<f32>(0.0031308));
}
/// The studio light summed in linear light, encoded once (#7974): the key and the eye fill as in
/// [`lit_color`], over a sky fill that grows as the face turns up, and the 4% every dielectric
/// reflects of that sky (a little more toward grazing), so a near-black face keeps a gradient. The sky
/// and the fill are what ambient occlusion (`visibility`) takes away. sRGB out.
fn sky_lit(base: vec3<f32>, n: vec3<f32>, diff: f32, fill: f32, visibility: f32) -> vec3<f32> {
let up = dot(n, normalize(u.sky.xyz)) * 0.5 + 0.5;
let sky = mix(0.18, 0.42, up);
// Schlick's rise toward grazing, tempered so mid-tones only gain a soft rim.
let fresnel = 0.04 + 0.2 * pow(1.0 - fill, 5.0);
let albedo = srgb_decode(base);
// Scaled so light and mid-grey faces land near where the sRGB shading puts them.
let light = albedo * (0.41 * diff + (0.66 * sky + 0.2 * fill) * visibility) + vec3<f32>(fresnel * sky * visibility);
return srgb_encode(light);
}
/// [`lit`] in a colour of its own: a whole highlighted body is lit in its colour lifted by the
/// hover amount, or in the selection colour, from the model's own plan triangles.
fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
@@ -485,11 +670,25 @@ fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
if u.product_lights_meta.x>0.0 {
var radiance=vec3<f32>(0.0);
for (var k=0u;k<min(u32(u.product_lights_meta.y),21u);k+=1u) {
if u.product_lights[k*4u].w == 4.0 {radiance+=to_linear(base.rgb)*u.product_lights[k*4u+1u].xyz/3.141592653589793;continue;}
let sample=product_light_sample(k,i.wp);
radiance+=to_linear(base.rgb)*sample[1]*(max(dot(n,sample[0]),0.0)/3.141592653589793);
}
let mapped=radiance/(vec3<f32>(1.0)+radiance);
let encoded=select(mapped*12.92,1.055*pow(mapped,vec3<f32>(1.0/2.4))-vec3<f32>(0.055),mapped>vec3<f32>(0.0031308));
return out_color(vec4<f32>(encoded,base.a));
}
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
let diff = max(dot(n, key), 0.0);
let fill = max(dot(n, v), 0.0);
let h = normalize(key + v);
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
if u.sky.w > 0.5 {
return out_color(vec4<f32>(min(sky_lit(base.rgb, n, diff, fill, visibility) + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
}
let k = 0.42 + 0.38 * diff + 0.25 * fill - (0.42 + 0.25 * fill) * (1.0 - visibility);
return out_color(vec4<f32>(min(base.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
}
@@ -654,38 +853,96 @@ fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
return out_color(i.c);
}
// Point clouds: each point is a round dot a few pixels across, facing the screen, depth-tested
// and written like the model's faces, so the model hides the points behind it and they hide it.
// The colour's alpha carries the dot's size (eighths of a pixel).
// Point clouds (`view/clouds.rs`): each point is a round dot a few pixels across, facing the
// screen, depth-tested and written like the model's faces, so the model hides the points behind
// it and they hide it. The cloud's style colours them by their own colour, a ramp or a palette,
// and shows only those whose filtered values lie in its ranges or sets.
struct POut {
struct Cloud {
style: vec4<f32>, // x: colour (0 own, 1 ramp, 2 palette), y: dot size (px), z, w: ramp range
filters: array<vec4<f32>, 4>, // x: kind (0 off, 1 range y…z, 2 whole values y + i for the bits i of w)
};
@group(1) @binding(3) var<uniform> cloud: Cloud;
struct CIn {
@location(0) p: vec3<f32>,
@location(1) v: f32,
@location(2) f: vec4<f32>,
@location(3) rgb: vec4<f32>,
};
struct COut {
@builtin(position) pos: vec4<f32>,
@location(0) c: vec4<f32>,
@location(1) wp: vec3<f32>,
@location(2) at: vec2<f32>,
@location(0) wp: vec3<f32>,
@location(1) v: f32,
@location(2) f: vec4<f32>,
@location(3) rgb: vec3<f32>,
@location(4) at: vec2<f32>, // the place on its disc
@location(5) @interpolate(flat) has: u32,
};
@vertex
fn vs_point(@builtin(vertex_index) vi: u32, @location(0) p: vec3<f32>, @location(1) c: vec4<f32>) -> POut {
fn vs_cloud_dot(@builtin(vertex_index) vi: u32, i: CIn) -> COut {
var xs = array<f32, 6>(-1.0, 1.0, 1.0, -1.0, 1.0, -1.0);
var ys = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let at = vec2<f32>(xs[vi], ys[vi]);
let size = max(c.a * 255.0 / 8.0, 1.0);
let centre = u.vp * vec4<f32>(p, 1.0);
let half = u.screen.xy * 0.5;
var o: POut;
o.pos = vec4<f32>(centre.xy + at * size * 0.5 / half * centre.w, centre.z, centre.w);
let size = max(cloud.style.y, 1.0);
let centre = u.vp * vec4<f32>(i.p, 1.0);
var o: COut;
o.pos = vec4<f32>(centre.xy + at * size / u.screen.xy * centre.w, centre.z, centre.w);
// Behind the eye: nothing.
if (near_z(centre) < 0.0) { o.pos = vec4<f32>(0.0, 0.0, 0.0, -1.0); }
o.c = vec4<f32>(c.rgb, 1.0);
o.wp = p;
o.wp = i.p;
o.v = i.v;
o.f = i.f;
o.rgb = i.rgb.rgb;
o.has = u32(round(i.rgb.a * 255.0));
// Dots under two pixels are squares: a disc that small loses its centre pixel.
o.at = at * select(1.0, 0.0, size < 2.0);
return o;
}
@fragment
fn fs_point(i: POut) -> @location(0) vec4<f32> {
if (clipped(i.wp) || dot(i.at, i.at) > 1.0) { discard; }
return out_color(i.c);
/// Whether filter `f` keeps the value `v`.
fn cloud_keeps(f: vec4<f32>, v: f32) -> bool {
if (f.x < 0.5) { return true; }
if (f.x < 1.5) { return v >= f.y && v <= f.z; }
let k = i32(round(v)) - i32(round(f.y));
return k >= 0 && k < 32 && ((bitcast<u32>(f.w) >> u32(k)) & 1u) != 0u;
}
fn cloud_ramp(t: f32) -> vec3<f32> {
var stops = array<vec3<f32>, 5>(
vec3<f32>(40.0, 70.0, 200.0), vec3<f32>(30.0, 170.0, 230.0), vec3<f32>(60.0, 200.0, 90.0),
vec3<f32>(240.0, 220.0, 60.0), vec3<f32>(230.0, 70.0, 50.0));
let k = clamp(t, 0.0, 1.0) * 4.0;
let j = min(u32(k), 3u);
return mix(stops[j], stops[j + 1u], k - f32(j)) / 255.0;
}
fn cloud_palette(v: f32) -> vec3<f32> {
var colours = array<vec3<f32>, 10>(
vec3<f32>(240.0, 140.0, 40.0), vec3<f32>(245.0, 205.0, 65.0), vec3<f32>(230.0, 65.0, 65.0),
vec3<f32>(165.0, 75.0, 190.0), vec3<f32>(65.0, 190.0, 115.0), vec3<f32>(75.0, 150.0, 220.0),
vec3<f32>(240.0, 120.0, 180.0), vec3<f32>(140.0, 105.0, 75.0), vec3<f32>(120.0, 215.0, 215.0),
vec3<f32>(190.0, 190.0, 80.0));
if (v < -0.5) { return vec3<f32>(150.0, 160.0, 176.0) / 255.0; }
return colours[u32(round(v)) % 10u] / 255.0;
}
@fragment
fn fs_cloud(o: COut) -> @location(0) vec4<f32> {
var out = clipped(o.wp) || dot(o.at, o.at) > 1.0;
for (var k = 0u; k < 4u; k++) {
out = out || (((o.has >> k) & 1u) != 0u && !cloud_keeps(cloud.filters[k], o.f[k]));
}
if (out) { discard; }
var c = o.rgb;
let mode = cloud.style.x;
if ((o.has & 16u) != 0u && mode > 1.5) {
c = cloud_palette(o.v);
} else if ((o.has & 16u) != 0u && mode > 0.5) {
c = cloud_ramp((o.v - cloud.style.z) / max(cloud.style.w - cloud.style.z, 1e-12));
}
return out_color(vec4<f32>(c, 1.0));
}
+59
View File
@@ -75,6 +75,7 @@ impl Tokens {
font_compass: 13.5,
font_subtitle: 15.0,
font_intro: 16.0,
font_page_heading: 20.0,
font_article_heading: 17.0,
font_title: 22.0,
font_display: 26.0,
@@ -175,6 +176,8 @@ impl Tokens {
body: Color32::from_rgb(176, 186, 198),
body_edge: Color32::from_rgb(40, 44, 52),
manipulator: Color32::from_rgb(40, 110, 220),
joint_manipulator: Color32::from_rgb(235, 157, 0),
joint_axis: Color32::from_rgb(0, 95, 110),
preview_add: Color32::from_rgb(112, 160, 226),
preview_new: Color32::from_rgb(240, 225, 105),
preview_sketch: Color32::from_rgb(32, 190, 96),
@@ -193,6 +196,8 @@ impl Tokens {
sketch_halo: Color32::from_rgb(240, 243, 247),
sketch_point: Color32::BLACK,
sketch_locked: Color32::from_rgb(0, 140, 60),
// The error red (`error`).
sketch_conflict: Color32::from_rgb(214, 54, 54),
// Sketch dimensions in a plum of their own, apart from the blue and black curves for
// every colour vision mode (#7861).
dimension: Color32::from_rgb(130, 42, 110),
@@ -222,6 +227,31 @@ impl Tokens {
dialog_label: Color32::from_rgb(58, 70, 86),
selected_ink: Color32::from_rgb(26, 80, 127),
selected_bg: Color32::from_rgb(232, 241, 250),
figure_ground: Color32::from_rgb(247, 248, 250),
figure_line: Color32::from_rgb(111, 124, 143),
figure_highlight: Color32::WHITE,
figure_shadow: Color32::BLACK,
figure_face_top: Color32::from_rgb(255, 255, 255),
figure_face_left: Color32::from_rgb(228, 233, 240),
figure_face_right: Color32::from_rgb(201, 210, 222),
figure_card: Color32::from_rgb(255, 255, 255),
figure_white: Color32::WHITE,
figure_black: Color32::BLACK,
figure_accent: Color32::from_rgb(35, 110, 173),
figure_reference: Color32::from_rgb(138, 83, 0),
figure_axis_x: Color32::from_rgb(214, 69, 69),
figure_axis_y: Color32::from_rgb(46, 158, 91),
figure_axis_z: Color32::from_rgb(47, 111, 208),
figure_ok: Color32::from_rgb(47, 125, 91),
figure_warn: Color32::from_rgb(201, 138, 0),
figure_conflict: Color32::from_rgb(184, 74, 0),
pref_well: Color32::from_rgb(238, 241, 245),
pref_note: Color32::from_rgb(183, 74, 0),
pref_note_fill: Color32::from_rgb(251, 235, 221),
pref_on_accent: Color32::from_rgb(255, 255, 255),
pref_ok: Color32::from_rgb(39, 107, 44),
pref_ok_fill: Color32::from_rgb(229, 242, 230),
pref_disabled: Color32::from_rgb(138, 148, 163),
timeline_item: Color32::WHITE,
timeline_item_error: Color32::from_rgb(250, 220, 220),
timeline_marker: Color32::from_rgb(70, 76, 88),
@@ -341,14 +371,37 @@ impl Tokens {
t.dialog_label = t.text;
t.selected_ink = Color32::from_rgb(156, 199, 245);
t.selected_bg = Color32::from_rgb(42, 70, 110);
t.figure_ground = Color32::from_rgb(28, 31, 38);
t.figure_line = Color32::from_rgb(163, 174, 194);
t.figure_face_top = Color32::from_rgb(107, 119, 144);
t.figure_face_left = Color32::from_rgb(81, 91, 114);
t.figure_face_right = Color32::from_rgb(57, 66, 86);
t.figure_card = Color32::from_rgb(47, 52, 62);
t.figure_accent = Color32::from_rgb(140, 190, 245);
t.figure_reference = Color32::from_rgb(232, 194, 74);
t.figure_axis_x = Color32::from_rgb(242, 122, 122);
t.figure_axis_y = Color32::from_rgb(98, 212, 142);
t.figure_axis_z = Color32::from_rgb(108, 168, 247);
t.figure_ok = Color32::from_rgb(127, 209, 174);
t.figure_warn = Color32::from_rgb(232, 194, 74);
t.figure_conflict = Color32::from_rgb(240, 162, 122);
t.pref_well = Color32::from_rgb(47, 52, 62);
t.pref_note = Color32::from_rgb(240, 162, 122);
t.pref_note_fill = Color32::from_rgb(74, 51, 38);
t.pref_on_accent = Color32::from_rgb(15, 22, 32);
t.pref_ok = Color32::from_rgb(127, 209, 174);
t.pref_ok_fill = Color32::from_rgb(31, 58, 49);
t.pref_disabled = Color32::from_rgb(138, 148, 163);
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
t.error = Color32::from_rgb(240, 100, 96);
t.sketch_conflict = t.error;
t.warning = Color32::from_rgb(236, 170, 70);
t.icon = Color32::from_rgb(208, 214, 226);
t.timeline = Color32::from_rgb(33, 37, 45);
t.hover = Color32::from_rgb(54, 61, 74);
t.hover_edge_halo = Color32::from_rgb(203, 203, 203);
t.origin_point = Color32::from_rgb(214, 215, 218);
t.joint_axis = Color32::from_rgb(110, 240, 250);
t.viewport_top = Color32::from_rgb(46, 52, 64);
t.viewport_bottom = Color32::from_rgb(46, 52, 64);
t.grid = Color32::from_rgb(59, 65, 76);
@@ -413,6 +466,9 @@ impl Tokens {
}
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_RED_GREEN[dark as usize]);
t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_RED_GREEN[dark as usize]);
// A deeper red keeps conflicting sketch geometry (#7244) apart from the orange
// construction and centerline lines and the crimson lost links for a red-green loss.
t.sketch_conflict = Color32::from_rgb(195, 35, 30);
t.surface_accent = if dark { Color32::from_rgb(200, 135, 55) } else { Color32::from_rgb(164, 80, 0) };
t.mesh_accent = if dark { Color32::from_rgb(190, 175, 70) } else { Color32::from_rgb(130, 103, 0) };
t.form_accent = if dark { Color32::from_rgb(165, 135, 220) } else { Color32::from_rgb(115, 73, 205) };
@@ -443,6 +499,9 @@ impl Tokens {
t.preview_sketch = Color32::from_rgb(125, 180, 105);
t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_BLUE_YELLOW[dark as usize]);
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_BLUE_YELLOW[dark as usize]);
// Conflicting sketch geometry (#7244) apart from the lost links and the construction and
// centerline lines for a blue-yellow loss: darker on the light view, lighter on the dark.
t.sketch_conflict = if dark { Color32::from_rgb(255, 170, 160) } else { Color32::from_rgb(165, 15, 30) };
if dark {
t.sketch_locked = Color32::from_rgb(140, 225, 110);
t.box_crossing = Color32::from_rgb(240, 110, 170);
+5
View File
@@ -1,4 +1,6 @@
{
"order": 76,
"provenance": "Original black-and-white palette",
"base": "dark",
"colors": {
"accent": "#ffffff",
@@ -91,6 +93,8 @@
"icons.highlight": "#000000",
"icons.marker_halo": "#000000",
"interference": "#ffffff",
"joint_axis": "#ffffff",
"joint_manipulator": "#ffffff",
"logo": [
"#ffffff",
"#ffffff",
@@ -142,6 +146,7 @@
"sketch": "#ffffff",
"sketch_accent": "#ffffff",
"sketch_centerline": "#ffffff",
"sketch_conflict": "#ffffff",
"sketch_construction": "#ffffff",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
+5
View File
@@ -1,4 +1,6 @@
{
"order": 75,
"provenance": "Original black-and-white palette",
"base": "light",
"colors": {
"accent": "#000000",
@@ -91,6 +93,8 @@
"icons.highlight": "#ffffff",
"icons.marker_halo": "#ffffff",
"interference": "#000000",
"joint_axis": "#000000",
"joint_manipulator": "#000000",
"logo": [
"#000000",
"#000000",
@@ -142,6 +146,7 @@
"sketch": "#000000",
"sketch_accent": "#000000",
"sketch_centerline": "#000000",
"sketch_conflict": "#000000",
"sketch_construction": "#000000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
+2
View File
@@ -1,4 +1,6 @@
{
"order": 220,
"provenance": "Maps the Okibi design system",
"base": "light",
"colors": {
"accent": "#c63a00",
@@ -1,4 +1,6 @@
{
"order": 110,
"provenance": "Original SolidCraft theme",
"base": "dark",
"colors": {
"accent": "#ffbc48",

Some files were not shown because too many files have changed in this diff Show More