Compare commits

..

180 Commits

Author SHA1 Message Date
bherbruck 18a734d029 Add a trademark policy for the SolidCraft name and logo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:19:36 -04:00
github-actions[bot] 48bcc83e2e Regenerate generated files and fold pending files [skip ci] 2026-10-28 14:56:01 +00:00
austin62jake 1e3cb3e36e Motion interference: a check cut short is not clear, and budget_s gives it time (#7855) (#7934)
* claim: #7855

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

* Motion interference: a check cut short by its work budget is not clear (#7855)

motion.interference stops when its work budget runs out (truncated) but
answered clear: collisions.is_empty(), so a check that reached only the first
steps read as a pass: a gear pair checked for 14 of 61 samples was "clear".
clear is now true only when every sample was checked and none collide.

The budget is an argument of interference_within, so a test can cut a check
short: B slides up to touching A, a full check is clear, and the least work
that checks a first step stops after 1 of 16 samples with clear: false (it
was clear: true before). The guide and the mechanisms skill say the same.

The issue's second request, a budget_s on motion.interference, is not here.

Refs #7855.

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

* Motion interference: budget_s gives a check the time it needs (#7855)

The work budget is fixed, so a gear pair drawn in detail was cut short
after a few samples, or refused before its first one. motion.interference
now takes budget_s (seconds, above 0, at most 86400, as inspect.interference
allows): the check then runs until that time instead of the work budget,
looked at before each sample, and echoes budget_s in its answer. Without
budget_s nothing changes. The refusal before a first step now names the
budget that ran out and suggests budget_s.

Tests: given a time, the smallest work budget (which checks no step) no
longer stops the check, through motion.interference too; budget_s 0, -1,
1e9 and "soon" are refused.

Fixes #7855.

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-28 12:19:10 +00:00
Inguss 4d6664e4ca fix(ui): expose Redefine Sketch Plane in History menus (#8024) 2026-10-28 12:18:53 +00:00
raykholo d56dc310b0 Browser: Collapse All keeps the clicked row open (#8032) (#8033)
Collapse All from a component or placement row folds everything under that
row and leaves the row itself open, matching the whole-tree Collapse All,
which already keeps the design row open.
2026-10-28 12:18:28 +00:00
semmelrinde 4ab612ea67 Standard Parts and Contact Set: label the rows in words, not with raw keys (#8005)
* Standard Parts and Contact Set: label the rows in words, not with raw keys

The Insert Part dialog listed a size's dimensions under the library's
keys (d, head height, socket depth), and the Contact Set dialog's
checkbox read "enabled". The dimensions now take messages (Nominal
diameter, Head height, Socket depth, ...) and the checkbox reads
Enabled, translated like the rest of the dialogs.

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

* Contact Set: translate the Enabled checkbox's label

ui_kit::checkbox paints the text it is given; the callers translate it,
as Configuration Rules' Enabled does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:17:07 +00:00
semmelrinde 73f06753b6 Swedish: translate 946 messages the catalog was missing (batch 1) (#8181)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:16:54 +00:00
semmelrinde 292177e085 Preferences: the tab colour strip names the tabs as the ribbon does (#8027)
The Interface page's strip of tab accent colours looked up its own
word-cased names ("Sheet Metal", "Surface"), which the catalogs
mostly don't carry or carry with another meaning: in German it read
"Sheet Metal" and "Flächenkörper" where the ribbon says Blech and
Fläche. It now names each tab the way the ribbon does, through the
tab's heading message, and the Form tab gets one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:16:48 +00:00
Colin Faulkingham d7111c7326 Ribbon: keep workspace captions inside the switcher (#7931) (#7956)
* claim: #7931

* Fix translated workspace captions overflowing the switcher (#7931)
2026-10-28 12:16:19 +00:00
github-actions[bot] 745184f2fc Regenerate generated files and fold pending files [skip ci] 2026-10-28 14:26:54 +00:00
semmelrinde 942f11169c Preferences: the snap-suppress choice and Direct interaction in the user's language (#8067)
* Preferences: the snap-suppress choice and Direct interaction in the user's language

The Hold to stop snapping choice joined its keys with an English "or"
("Cmd or Option", the default) in every language, and Navigation's
Direct interaction setting and its note had no message. The choice now
reads through a message with the keys as placeholders, and the setting
gets its two messages.

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

* Move the new test away from where another open PR adds one

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:48 +00:00
semmelrinde 029cf29b49 Dialogs: 54 labels that read English in every language get messages (#8049)
* Dialogs: 54 labels that read English in every language get messages

The construction dialogs' selection fields (First edge, Second point,
Through point, ...), the joint types (Rigid, Revolute, Slider, ...), the
Sheet Metal and Plastic rules' rows (K Factor, Flange Gap, Min Draft,
...) and a few options and prompts are passed to the label translator as
English source text that no catalog carried, so they showed English in
every language. They are added in one pending file, with the eleven
usual translations, and a test makes sure every dialog's selection
fields keep a message.

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

* Dialog labels: German Flange Gap is Flanschspalt, as the catalog's Flansch

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:35 +00:00
semmelrinde 7ad059aec9 Italian: translate 719 messages the catalog was missing (batch 1, all but the Render workspace) (#8062)
The Italian catalog lacked 1350 of the English messages. This batch
translates the 719 outside the Render workspace: Preferences, the
Assistant, the help texts, command labels, materials, navigation and
the rest. It uses the catalog's own terms (Schizzo, Lavorazione, Corpo,
Giunto, Cronologia, Struttura, Risalto, Appoggio, Aggancio a scatto).
The Render workspace's 600 messages are left for a second batch while
that workspace is still being built.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:24 +00:00
github-actions[bot] a2a00aa9ee Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:58:14 +00:00
github-actions[bot] 59551c59ab Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:49:47 +00:00
semmelrinde bf1b09698c Insert Part: the part families and the Name hint in the user's language (#8018)
* Insert Part: name the part families in the user's language

The Type list of Insert Part and the Change Part Size dialog showed the
library's English family names (Socket head cap screw, Hex nut, ...) in
every language. Each family now has a message under its library id,
and a family the catalogs don't know keeps the library's name.

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

* Insert Part: the Name field's automatic hint in the user's language

The hint was the English word "automatic"; it now takes the catalogs'
parts.automatic_name, which every language already translates.

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

* Move the family-name helper and its test away from where #8005 adds its own

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:15 +00:00
semmelrinde fc2142b407 Turkish: translate the 200 messages the catalog was missing (#8070)
* Turkish: translate the 113 messages the catalog was missing

The Turkish catalog lacked 113 of the English messages: the newer
Preferences settings, the hole presets, the slicer messages, the
Assistant's start-up options and tool summaries, and the Render
workspace's stage, shots and inspector labels. They are translated in
one pending file, with the catalog's own terms.

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

* Translate the 87 messages main added since (the navigation bar's)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:03 +00:00
semmelrinde d3b2c715d3 Dialogs: translate the texts a few dialogs draw themselves (#8056)
* Dialogs: translate the texts a few dialogs draw themselves

The joints' Play/Stop buttons, the Motion Study's Loop and the BOM's
sub-parts checkboxes, the Stitch tolerance hint, the Flange type and the
Emboss wrap note were drawn as English literals; the joint lists named
each joint's type by its Rust name (PinSlot, PointOnPlane). They now take
messages, and the joint lists use the Type box's label, translated.

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

* Move the new test away from where another open PR adds one

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:51 +00:00
semmelrinde 09308fceb1 Dialogs: 96 hints and refusals that read English in every language get messages (#8083)
The Surface, Sheet Metal, Plastic, Frame, Move, Align, Assembly, Cam,
Coupling, Sweep, Part Size and sketch editing dialogs refuse their
inputs with English sentences ("select bodies first", "pick faces of
one body"). The dialog translates a refusal through the catalogs as it
shows it, but no catalog carried these 96, so they showed English in
every language. They are added in one pending file with the eleven
usual translations.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:41 +00:00
semmelrinde 8f87509f7d Russian: translate 841 messages the catalog was missing (batch 1) (#8077)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:34 +00:00
semmelrinde c962e5e8cd Traditional Chinese: translate 897 messages the catalog was missing (batch 1) (#8078)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:33 +00:00
semmelrinde 7df858594d Indonesian: translate 898 messages the catalog was missing (batch 1) (#8098)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:22 +00:00
semmelrinde fed9605357 Sketch: the status in the view and the browser's editing mark in the user's language (#8084)
* Sketch: the status in the view and the browser's editing mark in the user's language

While a sketch is edited, the view's corner said "Sketch1 — 2 degrees of
freedom" (or "fully constrained", "constraints conflict") and the
browser marked the sketch "(editing)" or "(attachment lost)", in
English whatever the language; one degree of freedom read "1 degrees of
freedom". They now take messages: the catalogs' existing degrees-of-
freedom ones and four new ones.

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

* Sketch status and editing mark: the sketch's default name as shown elsewhere

The browser shows a default name in the user's language ("Skizze 1")
except on the edited sketch's row, whose mark made it a different text;
the view's status kept the stored name too. Both now use the name as the
browser shows it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:38 +00:00
semmelrinde bf1886e90a Motion dialogs: translate the notes and refusals they draw themselves (#8096)
Motion Study, Motion Trace, the motion plot, Explode and the contact
check drew their notes and refusals as they got them ("add a key for a
joint first", "select a point on a component", "none"), so they read
English in every language. They now go through the label translator, and
the four that no catalog carried get messages.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:38 +00:00
phanoob dfa9afd39f Sketch: leaving a sketch opened inside a component gives the active component back (#8127) (#8157)
Double-clicking a sketch row inside a component (an inserted fastener's sketch)
activates that component so the sketch is edited in its placement. Finish and
Cancel only closed the sketch, so the component stayed active and every other
body stayed faded.

Edit Sketch now remembers the component and placement active before it
activated the sketch's own (also when sketch.edit activates the component of a
face-attached sketch), and leaving the sketch makes that one active again, as
Finish already puts the history marker back. A component activated while the
sketch is open stays active.

Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:15 +00:00
semmelrinde 944c658c90 Czech: translate 867 messages the catalog was missing (batch 1) (#8188)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:06 +00:00
semmelrinde 8f8df03358 Navigation: Maya and HFSS mouse presets (#6474, #2742) (#8184)
Maya moves the camera with Alt and any button (left orbits, middle pans,
right zooms), middle drag also pans. HFSS pans with Alt+left drag and zooms
with Shift+Alt+left; its Ctrl+left orbit is the drag from empty space every
preset already has, so a Ctrl+click on geometry still adds to the selection.
The help texts are in the catalog for every locale that has the other
presets' help.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:05 +00:00
github-actions[bot] 92fc868aa9 Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:22:20 +00:00
github-actions[bot] 33281cf3bc Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:13:50 +00:00
semmelrinde d64816437a Dutch: translate 942 messages the catalog was missing (batch 1) (#8172)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:02 +00:00
semmelrinde 8210e94b6d Malay: translate 888 messages the catalog was missing (batch 1) (#8216)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:08 +00:00
semmelrinde f6d969f98a Hungarian: translate 946 messages the catalog was missing (batch 1) (#8199)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:06 +00:00
dudedagger42 59faca3850 Canvas and Decal: send the picked face, so a large design takes the image (#5592) (#5593)
* claim: #5592

* Canvas and Decal: send the picked face, so a large design takes the image (#5592)

A click with Canvas or Decal sent only the point, so the engine searched every face of every
body for it and gave up on a large design ("face capture work limit exceeded"). Send the
picked body, face index and path as Create Sketch does; canvas.decal takes that face pick too.

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

* Canvas and Decal test: drop the check that a point alone gives up

On upstream main a sketch on a face by its point alone no longer gives up on this design, so
the test keeps only what the picked face has to do: both images land on it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:01 +00:00
semmelrinde 29a2f33664 Galician: translate 888 messages the catalog was missing (batch 1) (#8214)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:10:58 +00:00
sascha2101 9d4d7932ee Sketch: offer an arc length dimension for a selected arc (#6039) (#6264)
* claim: #6039

* Sketch: offer an arc length dimension for a selected arc (#6039)

The arc length dimension already existed in the engine (sketch.dimension with
type arc_length), but nothing in the UI led to it: the Dimension tool and the
context menu always gave an arc its radius.

With exactly one arc selected, the sketch context menu now offers Arc Length
Dimension next to Sketch Dimension, which adds the dimension through the
existing command.

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

* test: move the arc length menu test up to avoid a merge conflict

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:10:45 +00:00
github-actions[bot] 54a207cb8b Regenerate generated files and fold pending files [skip ci] 2026-10-28 12:44:09 +00:00
github-actions[bot] 5bf5de49a5 Regenerate generated files and fold pending files [skip ci] 2026-10-28 12:35:35 +00:00
semmelrinde cee995e776 Norwegian Bokmål: translate 693 messages the catalog was missing (batch 1) (#8218)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:10:34 +00:00
Nathan Dane b86c75544e Appearance: read the look chain and replace a look everywhere in one undo step (#6428) 2026-10-28 12:10:31 +00:00
yhamil04 249318cffb Right-click opens the menu despite tiny pointer movement (#5517) (#7939)
* claim: #5517

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

* Right-click opens the menu despite tiny pointer movement (#5517)

A Magic Mouse moves the pointer a fraction of a point as it is clicked.
The right button orbited on any movement, and the menu needed exactly
zero movement in the release frame, so most right-clicks only nudged
the view. The right button now navigates once the press can no longer
be a click (egui's click distance and time), catching up with the
travel since the press, and a right-click is egui's click.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:08:27 +00:00
Colin Faulkingham 6f8ee10a86 Appearance: apply browser actions to all selected bodies (#7935) (#7941)
* claim: #7935

* fix: apply browser appearance changes to selected bodies (#7935)
2026-10-28 12:08:21 +00:00
N3MI-DG 30cb1238f7 Browser: reveal a placed body's row when it is picked or found (#7947) (#7948)
* claim: #7947

* Browser: reveal a placed body's row when it is picked or found (#7947)

The off-screen skip in `entry_row` keeps the reveal target's row alive so it
still has a rect to scroll to, but it matched `app.tree.reveal` (a world body
name) against `Entry::key` (the placement-local name). For a placed body those
never match, so the row returned early and the `scroll_to_rect` below it, which
does use `row_key`, was never reached. `app.tree.reveal` is then cleared at the
end of the frame, so there was no second chance.

Match the skip on `row_key` minus its `b:` prefix, as the `scroll_to_rect`
already does.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-28 12:08:01 +00:00
raykholo db999b2a98 Control protocol: the ui.prefs settings as a sorted list, one per line (#7747) (#7988) 2026-10-28 12:07:43 +00:00
semmelrinde e59decda95 Primitive dialogs: label the size fields in words, not with the parameter names (#7995)
The Box, Cylinder, Cone, Sphere, Torus and Coil dialogs labelled their
size rows with the command's parameter names (length, top_diameter,
section_size), in every language. Each row now takes a message: Length,
Top diameter, Major radius, Section size, translated as the rest of the
dialog is.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:07:37 +00:00
github-actions[bot] aca80f2054 Regenerate generated files and fold pending files [skip ci] 2026-10-28 10:25:10 +00:00
Nathan Dane 0b2efd60b9 Render browser: scene tree with Environment, Lights, Shots and Objects rows, resolved appearances and a Browser selection (#6427) 2026-10-28 09:58:57 +00:00
amolinari-autotex 54f613e4dd Preserve linked component materials and appearances (#7609) (#7616)
* Fix linked component materials and appearances (#7609)

* Linked components: the cached link read carries the source properties

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

* Update Links: a link without recorded inherited properties is read again

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

* Preserve authored linked-face overrides and apply cached source properties

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:33:49 +00:00
github-actions[bot] ae07e36a7d Regenerate generated files and fold pending files [skip ci] 2026-10-28 07:57:59 +00:00
Nathan Dane 18fe82ab26 Render workspace: focus view, compact docks, a Browser chip at narrow widths and a localisation pass (#7376) 2026-10-28 07:29:53 +00:00
github-actions[bot] 372badfc1b Regenerate generated files and fold pending files [skip ci] 2026-10-28 06:43:04 +00:00
Nathan Dane 1781b4d323 Render workspace: move the Inspector or Browser dock to either side with view.render_dock and a header menu (#7230) 2026-10-28 06:15:13 +00:00
github-actions[bot] cf5dc813e2 Regenerate generated files and fold pending files [skip ci] 2026-10-28 04:43:28 +00:00
Nathan Dane b0c8c466b0 Navigation bar: icons for visual styles, selection modes and display (#6585) (#6898)
* Navigation bar: slot model and one-row renderer (#6585)

* Navigation bar: customisation section in Preferences (#6585)

* Navigation bar: icons for visual styles, selection modes and display (#6585)

* Navigation bar: redraw the Bodies, Parts and Components selection icons (#6585)

* Navigation bar: refine the Ground shadow, Ambient occlusion, Parts and Components icons (#6585)

* Navigation bar: solid cube and a longer arc for the Ground shadow icon (#6585)

* Navigation bar: the owner accepts the new icons as final (#6585)
2026-10-28 04:16:44 +00:00
github-actions[bot] 85aaed2ccb Regenerate generated files and fold pending files [skip ci] 2026-10-28 03:48:50 +00:00
Nathan Dane 110efa3598 Render: allow picking in the product view with a Body, Face or Component filter and outline the selection in the product pass only (#6426) 2026-10-28 03:20:43 +00:00
P'tite Tête 9fb41f8378 feat: Start a new selection's preview without waiting for the obsolete one (#7167) (#7261)
- Leave the running preview behind when the selection moves to another setup, asked to stop
- Keep at most two obsolete previews running; streaming, tools and other inputs wait as before
- Keep no result of a cancelled watched evaluation in the shared evaluation cache
- Test the detaching, the cancel reaching the spawned job, and the cache guard

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-28 01:32:17 +00:00
bherbruck 79214b6b49 Release 0.25.1 (#7983)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:58:27 -04:00
github-actions[bot] 49c0faa7da Regenerate generated files and fold pending files [skip ci] 2026-10-28 00:50:43 +00:00
bherbruck 2cdfe050db Extrude to Object: a Join leaves the body it runs to alone when that body is in another component (#7975)
#6471 joined the body a To Object extent runs to into the tool, whatever its component.
A connector's boss extruded up to an enclosure's face (each in its own component) merged the
enclosure into the connector, and features referencing the swallowed body failed after it.
The body it runs to now joins only when it is in the feature's own component, or when the
feature names it as a target.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:48:28 -04:00
CamWight 303fddb20e Browser: Bodies folders only where they help: Always, Several bodies only (the default) or Never (#4217) (#4219)
* claim: #4217

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

* Browser: Bodies folders only where they help: Always, Several bodies only (the default) or Never (#4217)

A new preference on the Interface page, in the Layout section under Browser, sets when a
component's bodies sit in a Bodies folder of their own. Several bodies only lists a component's
lone body, when it has no groups, straight under its placement. Never lists every component's
bodies and groups there. Always keeps the folder as before. The design's own Bodies folder and a
folder hidden with its eye always stay.

One rule, browser::bodies_folded, decides it for the rows and for the menu. A folded folder
draws no row and stays open; its items move up one level, and their own eyes, hover and drops do
its jobs. While a folder is folded, its placement's menu takes the folder's Create Components from
Bodies and Group Selected; New Group and Show All were already there.

ui.prefs takes browser_bodies_folders (always, several, never) and refuses other values; saved
preferences read an unknown value as the default. The s37 scenario sets Always so that it still
checks the Bodies folder's chevron.

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

* Browser: Bodies folders in Arabic, Hebrew and Vietnamese too (#4217)

Arabic, Hebrew, Japanese, Vietnamese and Chinese are complete catalogues, so
every_complete_catalog_carries_every_english_message fails a new message any of them lacks. The
preference's four messages get their Arabic, Hebrew and Vietnamese text, in the words those
catalogues already use for a body and a folder.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:24:17 +00:00
github-actions[bot] 03606ec97b Regenerate generated files and fold pending files [skip ci] 2026-10-27 23:59:03 +00:00
taylorhayis c4fb42f665 Preferences: search settings (#2218) (#6002)
* claim: #2218

* Preferences: search settings across every page (#2218)

A field at the top of Preferences filters the live controls with the same scorer as command search, including a dropdown's current value and the English source of a translated label.

* Preferences search: keep the plain page list for its tests (#2218)

The production dialog always draws the search strip, so the no-header entry is test-only and no longer warns as unused.

* Preferences search: show only the settings that match (#2218)

A page name or a loose string of letters was keeping the rest of that page on screen, including notes and the mouse legend.

* Preferences search: drop stray chrome and clear from the field (#2218)

The colour swatches and folded Advanced heading were painted on every query, and the field had no way to empty itself.

* Preferences search: fold shortcut matches and drop the blank gaps (#2218)

* Preferences search: stop unmatched pages painting over the hit (#2218)

* Preferences search: a page tab clears the search and shows that page (#2218)

* Preferences search: keep OK and the graphics checkboxes in view (#2218)

A dropdown that skipped its real height on a dialog's first frame left OK outside the window, and the search strip used up the rows the graphics page needs. The shortcut fold is in the UI kit gallery.

* Preferences search: type a query and open the match from the keyboard (#2218)

A printable key or Cmd/Ctrl+F focuses Search settings, arrows move through the matches, and Enter opens that row. Escape clears the query before it can close the dialog, and clearing returns to the page that was up when the search started.

* Preferences search: translate the search strings for the complete catalogs (#2218)

Arabic, Hebrew and Vietnamese are delivered complete, so Search settings and the empty result need a translation there. German and Korean get the same two strings.
2026-10-27 23:27:33 +00:00
yhamil04 00e31c24fd Sketch: Delete on a picked profile deletes the curves around it (#7107) (#7120)
* claim: #7107

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

* Sketch: Delete on a picked profile deletes the curves around it (#7107)

While a sketch is being edited, a profile picked by clicking inside a
shape stands for the curves of its outer loop, so Delete removes the
shape instead of failing with "a profile goes with its sketch". Holes
inside it are shapes of their own and stay. Outside the sketch a
profile still goes only with its sketch.

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

* Delete a picked letter's whole text, not just its outline (#7107)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:04:41 +00:00
bherbruck 03b8f4138b New designs save as .sc; .solidcraft files still open (#7325)
* claim: new designs save as .sc

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

* New designs save as .sc; .solidcraft files still open and keep their name on Save

DESIGN_EXT is now "sc"; DESIGN_EXTS lists sc, solidcraft and solvecraft and
every open path (dialogs, drag-drop, recent files, CLI, MCP, links, the web
picker) accepts all three. Save of an opened .solidcraft file writes that
file; Save As, autosave, recovery and crash reports use .sc. Windows, macOS
and Linux register .sc alongside the older extensions.

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

* Update UI tests for the .sc default extension

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:41:40 +00:00
github-actions[bot] 613b468ef5 Regenerate generated files and fold pending files [skip ci] 2026-10-27 22:58:37 +00:00
yhamil04 c0a0da225a Ribbon tooltips: help for the Solid > Modify tools, and new toolbar commands must have it (#2758) (#7134)
* Ribbon tooltips: help for the Solid > Modify tools; new toolbar commands need it (#2758)

Writes the two-sentence help for the 16 Modify tools that had none (en, fr,
ja, zh), and adds a test that fails when a toolbar command has no help; the
190 older ones are on a backlog that only shrinks. AGENTS.md, CONTRIBUTING.md
and the Adding a command guide say so.

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

* Ribbon tooltips: the help test reads the ribbon as the toolbar lays it out (#2758)

Review: commands placed by a workspace section without .at(...) were not
checked; 79 of them join the backlog. Physical Material's help no longer
promises the centre of mass.

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

* Ribbon tooltips: French help uses the glossary terms (#2758)

Matériau, Apparence and Scinder, as rlartot asked in review; Delete Face
says the hole stays open.

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

* Ribbon tooltips: the help rule fits the commands skill's size limit (#2758)

AGENTS.md became a router to skills on main, so the rule lives in
skills/solidcraft-commands/SKILL.md, kept under its 3600-character limit.

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

* Ribbon tooltips: Arabic, Hebrew and Vietnamese help; the backlog follows main (#2758)

Arabic, Hebrew and Vietnamese are complete catalogs now, so the 16 texts
come in those too. Twelve toolbar commands that landed before the rule
join the backlog; the Render commands, which left the ribbon, leave it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:30:15 +00:00
bherbruck 03d0342469 Release 0.25.0: version bump, design format 41 (#7916)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:05:36 -04:00
github-actions[bot] 13d62705f3 Regenerate generated files and fold pending files [skip ci] 2026-10-27 21:51:37 +00:00
Frank 7fad8dbec0 Fix feature creation at timeline end (#7620)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-27 21:17:39 +00:00
Lars Jansen 83ac98ac76 Origin plane names: Top / Front / Right (#6491) (#6496)
* claim: #6491

* feat(ui): origin plane names Top / Front / Right (#6491)
2026-10-27 21:06:14 +00:00
sascha2101 42ec27cfe4 Browser with History under it: drag the split between the two cards (#7371) (#7803)
* claim: #7371

* Browser with History under it: drag the split between the two cards (#7371)

Under the browser, History took exactly half the column. The seam
between the two expanded cards is now a grip with the vertical resize
cursor; dragging it moves the split, as the column's right edge moves
its width. The split is kept as the browser's share of the column in
a new preference, browser_share, whose default is the equal split, so
nothing changes until someone drags.

Either card keeps at least its header and two rows. Folding either card
works as before and gives the other the rest; while one is folded or
folding there is no grip. A non-finite share falls back to one half.

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

* Prefs: keep the browser share with the History-under-browser settings

Moves the entry so it no longer sits next to the render and dialog
preferences that main added after browser_width.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:59:38 +00:00
semmelrinde ca75f86947 Sketch: deleting a dimension in a rolled-back history no longer rebuilds the features after the marker (#7253) (#7425)
* claim: #7253

* Sketch: deleting a dimension in a rolled-back history no longer rebuilds the features after the marker (#7253)

The check before a delete evaluated the whole history twice when the
timeline was rolled back. Removing a sketch constraint changes the later
features' cache keys, so on a long history Delete on a dimension took
minutes. When the design differs only in sketch constraints (and model
parameters nothing uses any more) and each changed sketch solves to the
same geometry, profiles and ends, the features after the marker see the
same inputs: compare the live models only. Anything else still gets the
full check, so the report is unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:53:34 +00:00
bherbruck 7a6c884418 Change the app id to io.tangencylabs.solidcraft, with aliases for existing installs (#7681)
The app id moves from ai.storyteller.solidcraft to io.tangencylabs.solidcraft: macOS bundle id
(the CLI signs as io.tangencylabs.solidcraft.cli), Linux desktop/metainfo/MIME file names, icon
name, Wayland app_id / X11 class, Flatpak, Snap common-id, nfpm/FreeBSD/Nix/AppImage staging,
the web app's icon paths and the docs.

Existing installs keep working: the Flatpak repository marks both old ids
(ai.storyteller.solidcraft and ai.storyteller.solvecraft) end-of-life with a rebase straight to
the new id, re-marking the solvecraft ref that pointed at the 0.8 id; the manifest grants
read-only access to both old ~/.var/app folders and the first launch copies settings and
autosaves from the newest one; AppStream <replaces>/<provides> and X-Flatpak-RenamedFrom list
both; the AppImage removes menu entries it wrote under either old id. The data and settings
folders (solidcraft / SolidCraft) are unchanged. The MSI UpgradeCode and ProgIDs are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:16:40 -04:00
github-actions[bot] a6ef3ee4d2 Regenerate generated files and fold pending files [skip ci] 2026-10-27 20:57:39 +00:00
github-actions[bot] 153af8da22 Regenerate generated files and fold pending files [skip ci] 2026-10-27 20:49:28 +00:00
semmelrinde 392f4ed396 Report a bug: Ctrl/Cmd+Enter continues on GitHub (#7203) (#7204)
* claim: #7203

* Report a bug: Ctrl/Cmd+Enter continues on GitHub (#7203)

The report's fields are multi-line, so Enter adds a line break and the
only way on was the mouse. Ctrl+Enter (Cmd+Enter on a Mac) in any field
now does what Continue on GitHub does: it is read before the fields see
it, so it adds no line break, and an empty description is still asked
for. A note under the description says so, with the keys of this
computer.

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

* Report a bug: keep the shortcut hint apart from the other report changes

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

* Report a bug: holding Ctrl/Cmd+Enter continues on GitHub once

egui's consume_key also matches the key's repeats, so every repeat frame
opened another GitHub tab. Only the press counts now; repeats are dropped.
The test sends the modifiers each platform really sends (mac_cmd or ctrl
together with command) and holds the key.

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

* Locales: Arabic and Hebrew for the new strings

ar and he are complete catalogs on main now (#3458): every new message
needs them. The pending-messages test compares right-to-left messages in
display order once #7532 lands (#7527).

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

* Locales: Vietnamese for the new strings

Vietnamese became a complete catalog on main (#5823), so every new English
message needs it too.

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

* Report a bug: the Save screenshot test scrolls its button into view

The shortcut hint adds a line above the screenshot, which moves its
buttons below the fold of the review's scroll area at the test's size.
The test scrolls them into view first, as the other report tests do.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:28:04 +00:00
Patrick Steinert 0e4c5ae577 Save as Mesh: Add all bodies and Add visible bodies buttons (#7297)
* Save as Mesh: Add all bodies and Add visible bodies buttons

Two buttons under the Bodies input fill it with every body of the design,
or with only the bodies the viewport shows.

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

* Save as Mesh: translate the two body buttons for every complete catalog

ar, he, ja, vi and zh are complete catalogs, so every English message needs
an entry there; ko as the ui-text skill asks.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 20:08:27 +00:00
raykholo d2f0627421 Follow a theme file and reload it when it changes (#7573) (#7574)
* claim: #7573

* Follow a theme file and reload it when it changes (#7573)

* Translate the follow-a-theme-file strings
2026-10-27 20:08:12 +00:00
semmelrinde e0e2c566e7 Viewport: clear new resolve targets so AMD Direct3D 12 shows the scene (#6828) (#7399)
* claim: #6828

* Viewport: clear new resolve targets so AMD Direct3D 12 shows the scene (#6828)

With anti-aliasing above 1x the scene is drawn multisampled and resolved
into textures that nothing but the resolve writes. wgpu counts the resolve
as their first write and never clears them. Direct3D 12 requires a render
target placed in reused memory to be cleared, discarded or copied over
first, and some AMD Radeon drivers then show the uncleared texture as
transparent, a flat colour or speckles: no grid, origin planes or bodies
(#6828, #7157, #6613, #7052). Each new resolve texture (the frame's colour
and the see-through sums) is now cleared once when it is made.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:01:40 +00:00
Dahak Builds ebdc0bc07e Assembly: deeply nested body selection no longer stalls the window (#7502) (#7694)
* claim: #7502

* Assembly: name placed bodies from a per-design index, reveal the Browser from the body's path, evict cache entries singly (#7502)

* Release the placement index before in-place document edits so pinning body names stays in place

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-27 19:44:52 +00:00
Sebastian Höpfl d1398e2c52 Evaluation: keep a sketch's frame bodies across evaluations that move nothing (#7814)
A sketch on a face of a placed component is evaluated in that component's frame, so the
evaluator moves (and meshes) the bodies placed differently into it. FrameBodies keeps those
moved bodies, but every evaluate started a new generation and dropped what neither it nor the
evaluation before it used. Evaluations that reuse every result never touch the cache, so two
of them in a row emptied it: Delete refreshes the model three times (selection.delete's nested
refresh, its own, then execute's), and the confirmation's dry run more, and an undo answered
from the cache is another. The next edit of the sketch then moved and meshed every placed body
again: in a design of ~3,500 imported bodies each sketch edit after a Delete took 46 s, 34-43 s
of it tessellation.

The generation now starts on the evaluation's first use of the cache, so an evaluation that
moves nothing leaves it alone. It still holds at most two generations of moved bodies.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:43:12 +00:00
Badger B 5bdb11bd56 Add German translations, batch 2 (#7237) (#7826)
* Regenerate generated files and fold pending files [skip ci]

* Add German translations, batch 2 (#7237)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-27 19:34:36 +00:00
Irakli Kourtis 69e45b88e7 docs: a disclaimer of warranty, liability, indemnity and harmful use (#3882)
Claude-Session: https://claude.ai/code/session_013Bhtfdu3WDoF5dQjzzdcbq

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:32:16 +00:00
Victor Santos Oliveira 5869f4c247 Windows: gate Unix-only slicer readiness states (#7696) (#7697)
* claim: #7696 slicer readiness platform guards

* fix(engine): gate platform-specific slicer readiness code
2026-10-27 19:28:34 +00:00
tr1pp4 98abb249c4 Cylinder: place by distances from reference edges, like Hole (#6380) (#6381)
* claim: #6380

* Cylinder: place by distances from reference edges, like Hole (#6380)

A Cylinder standing on a flat face can take up to two straight edges of
that face as references, each with the distance of its base centre from
the edge's line, measured in the face's plane, as a Single hole does
(#37). `solid.cylinder` takes them as `references: [{edge, distance}]`
with a numeric `base`; the edges are found again by their persistent
names, else their points, so the cylinder follows them when the part
changes, and each distance is a parameter (an input of Inspect Feature).
A cylinder without references stays at its base, as before.

In the Cylinder window, started on a picked face, Reference 1 and 2 take
the edges, each with its Distance measured from the pick, so it works
with the mouse alone; the canvas shows each distance with a box to type
it in, and Edit Feature picks the edges again.

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

* Cylinder: Edit Feature shows each reference edge input once (#6380)

A face selected when Edit Feature opened the window had already added
the reference inputs; the edit added them a second time.

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

* Cylinder references: the measured-once cost test's cylinder has none (#6380)

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

* Cylinder: pick the face it stands on in its window, then the edges (#6380)

The Cylinder window took its place only from a face selected before it
opened, so started first it stood at the origin and offered no reference
edges. It now has a Face input before Reference 1 and 2, as the Hole
window does: start Cylinder, click the face, click the edges, OK.

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

* Glossary: reference edge, and "Reference" among the words with more than one meaning (#6380)

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

* Cylinder edge references: the Cone dialog (#6250) gets the references field too

The Cone primitive landed on main after this branch began; its dialog initialisers lacked
the `refs` field the Primitive kind gained here. The dense form check for Cylinder fills only
its fields: its face and reference edges place it, which the reference tests cover.

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

* Cylinder: declare references in its contract; reference-placed cylinders are not placement-independent

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 19:28:22 +00:00
Daniel Dimitrov 7b03439dda Navigation: Free orbit can turn around the vertical axis (#6964) (#6965)
* Navigation: Free orbit can turn around the vertical axis (#6964)

A new option under Free orbit, Turn around the vertical axis, makes mouse
drags a turntable without the pole limit: sideways turns about the world's
up axis (reversed upside down so the model follows the cursor), up/down
tilts over the top. Off by default; free orbit without it is unchanged.

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

* i18n: Japanese and Chinese for the vertical-axis orbit option (#6964)

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

* i18n: Arabic, Hebrew, Vietnamese, German and Korean for the vertical-axis orbit option (#6964)

Main now treats ar, he and vi as complete catalogs, so the merge queue's
every_complete_catalog_carries_every_english_message failed on the two new ids.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: danieldimitrov86 <danieldimitrovdev@gmail.com>
2026-10-27 19:28:14 +00:00
Dahak Builds 74d1db902e Turkish locale: complete tr.json catalog (#6240) (#7668)
* claim: #6240

* i18n(tr): complete the Turkish catalog with 1280 missing messages

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-27 19:22:49 +00:00
github-actions[bot] 882737ff94 Regenerate generated files and fold pending files [skip ci] 2026-10-27 19:52:35 +00:00
Nathan Dane 295d04932f Render workspace: stage bar, aligned floating docks and the Shots strip host (#6281) (#6296)
* claim: #6281

* Render workspace: stage bar, aligned floating docks and the Shots strip host
2026-10-27 19:21:09 +00:00
semmelrinde 0d18757ed7 File dialogs open in the folder used last (#7756) (#7807)
* claim: #7756

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

* File dialogs open in the folder used last (#7756)

The desktop dialogs never said where to open, and the Linux ones then
start in the home folder every time. Open, Insert, attach, Save As and
Export now open in the folder of the file last chosen in a dialog, or of
the open design when another design becomes the open one, whichever came
last. Also fixes #7735.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:19:10 +00:00
Hainlein4711 9030d2ff19 Ribbon: wrap Japanese button labels across two lines (#7586) (#7669)
* claim: #7586

* UI: wrap Japanese ribbon button labels across two lines at CJK script boundaries (Fixes #7586)
2026-10-27 19:06:07 +00:00
raykholo 38820658fa Body appearance replaces imported STEP face colours (#5769) (#5775)
* claim: #5769

* Let body appearances replace imported STEP face colours (#5769)

A body appearance assigned in the design now replaces the face colours the
body was imported with; face appearances set in the design still win over it.
2026-10-27 19:03:10 +00:00
Erlis Vidal c8c5c882de Sketch: name = value in a drawing tool's value box defines the parameter (#7567) (#7660)
* claim: #7567

* Sketch: a drawing tool's value box takes `name = value` (#7567)

While drawing, a value box (circle diameter, line length, rectangle width and
height, slot, arc, polygon) now accepts `name = expression` and
`#name = expression`, as the Dimension tool already does: the shape is drawn at
the expression's size and its dimension defines and uses the user parameter
`name`, in one undo step.

- sketch.draw.dimensioned checks a definition's right-hand side, read as the
  new parameter will hold it (a bare number in the design's unit), and passes
  the text on to sketch.dimension; the reply lists `defined`.
- The value boxes size the preview and validate by the right-hand side, refuse
  a definition in a box that is not a dimension (polygon sides, rounded
  rectangle corner radius, conic rho), and the status line names the
  parameters defined.
- Guides: parameters and sketching.

Fixes #7567

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:02:09 +00:00
semmelrinde 5dc95ef7f6 Viewport: a frame the GPU has no memory for is drawn smaller, not a crash (#7296) (#7398)
* claim: #7296

* Viewport: a frame the GPU has no memory for is drawn smaller, not a crash (#7296)

The viewport's offscreen frame, its see-through targets and the cached
still picture were created with no error scope, so a texture the card had
no video memory for went to wgpu's default handler and closed the app
("wgpu error: Out of Memory" on a GT 740). They are now created under error
scopes; a refused frame lowers the renderer's frame budget and is asked for
again smaller (less supersampling, or the view's own frame drawn smaller and
filtered up), refused OIT targets fall back to sorted see-through surfaces,
and the status line says once that the view is drawn at a lower quality.

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

* Locales: Arabic and Hebrew for the new strings

ar and he are complete catalogs on main now (#3458): every new message
needs them. The pending-messages test compares right-to-left messages in
display order once #7532 lands (#7527).

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

* GPU memory test: targets take the OIT flag main added (#7296)

The test's two GpuTarget initializers predate the oit field and no longer
compiled once the branch met main.

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

* Locales: Vietnamese for the new strings

Vietnamese became a complete catalog on main (#5823), so every new English
message needs it too.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:58:31 +00:00
sascha2101 e2d409e6ed Save: a design changed while it is written stays unsaved, and scripts wait for it (#6003) (#7619)
* claim: #6003

* Save: a design changed while it is written stays unsaved, and scripts wait for it (#6003)

Two of the gaps @raykholo found in #6003 were still open on main:

- `engine.script` ran while the window's Save was being written: it calls
  the session directly, past `SolveApp::run`'s file-operation guard, and
  `drain_control` did not hold design-changing requests for a save. They
  now wait for the save like they wait for a file being loaded or picked.
- `adopt_save` marked whatever the session held when the save finished as
  saved. It now takes up the state the save wrote (`done.saved`, and its
  visibility), so a change made meanwhile stays unsaved instead of being
  lost on close without a prompt.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:52:36 +00:00
selune 3179cd2ebd Sketch text: cover line pitch and format-12 glyph zero (#6570) (#6574)
* claim: #6570

* test(text): cover nonpositive pitch and format-12 glyph zero (#6570)

Original synthetic font regressions. Focused native font tests, strict scoped Clippy and wasm pass. Full repository gate status is recorded in PR #6574; no CI infrastructure changes.
2026-10-27 18:51:44 +00:00
phanoob d139e415e4 Sketch: switch a circle or arc dimension between radius and diameter (#7499) (#7729)
* Sketch: switch a circle or arc dimension between radius and diameter (#7499)

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

* Sketch: Toggle Radius / Diameter for a circle's or an arc's dimension (#7499)

New command sketch.toggle_diameter, with input and output schemas, and a
context-menu item on a single selected radius or diameter dimension. The
circle keeps its size: the dimension's parameter is halved or doubled (a
plain value is rewritten, an expression is wrapped). Refused, with nothing
changed, when anything else uses that parameter.

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-27 18:49:30 +00:00
semmelrinde d14dbe5e39 Move: a copying Move can be edited from the History (#7741) (#7761)
* claim: #7741

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

* Move: a copying Move can be edited from the History (#7741)

Editing a feature runs its command on a scratch session with the marker
right after it. For the last feature that marker sits at the end of the
timeline, and place_feature pushed the new feature after it, rolled back,
so a copying Move found no copies and refused the edit. A marker right
after the last feature now moves on past a feature added there.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:47:01 +00:00
Igor Bronovskyi 143145751c i18n(uk): add pending translations for missing strings (#7556) 2026-10-27 18:46:33 +00:00
semmelrinde 98658142c0 Delete: deleting one instance of a component takes its joints with it (#7749) (#7782)
* claim: #7749

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

* Delete: deleting one instance of a component takes its joints with it (#7749)

Deleting an occurrence while its component had other instances dropped
only the occurrence: the joints on it, their motion links, joint origins,
contact set places and exploded moves stayed, so a joint stayed in the
Browser naming an occurrence that was gone. The clean-up that
remove_component_tree did is now Document::remove_occurrences, used by
both.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:44:44 +00:00
github-actions[bot] ee6ec09b0a Regenerate generated files and fold pending files [skip ci] 2026-10-27 19:07:17 +00:00
github-actions[bot] 0accea759d Regenerate generated files and fold pending files [skip ci] 2026-10-27 18:59:13 +00:00
HiroKenEz 8962ae10e7 Box dialog: place the box by its corner or centre (#5638) (#6248)
* claim: #5638

* Box dialog: place the box by its corner or centre (#5638)

The Box dialog gets a Placement choice (Corner or Center) with X, Y and Z
fields that take expressions and units, and a Point input: a vertex, a
point on an edge or face, or the origin picked in the view fills them.
The Cylinder dialog gets X, Y and Z for the centre of its base. Left
alone, a new dialog sends no placement, so the box stays at the origin;
Edit Feature shows the stored point and OK without a change keeps it.

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

* Box dialog test: pass the active feature to Input::accepts

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

* Box dialog: Base Center in the complete Arabic, Hebrew and Vietnamese catalogs (#5638)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 18:29:05 +00:00
semmelrinde 28b27aac8f Saved model: kept only when every body decodes to itself (#6760) (#6761)
The native codec gave one body back with other topology: a pattern copy
cut by an automatic extrude, 10 faces, 18 edges and 10 vertices, came
back with 12 vertices, so the design opened with another body than it
was saved with. Encoded::of now decodes each body it encodes and keeps
the model only when every body comes back with the same face, edge,
vertex and shell counts; otherwise the design is saved without its
model, as when the encoder refuses a body, and opening it evaluates it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:28:41 +00:00
Lars Jansen 5a477b7774 Dialogs: open in the left column in place of the browser and History (#6707) (#6712)
* claim: #6707

* feat(dialogs): open in the left column in place of the browser and History (#6707)

* fix(i18n): Vietnamese messages for the dialog placement option (#6707)
2026-10-27 17:59:00 +00:00
raykholo 236b5c9ebf Add the built-in 1-bit and 1-bit Dark themes (#7550) (#7557) 2026-10-27 17:46:31 +00:00
github-actions[bot] 95ff76cb97 Regenerate generated files and fold pending files [skip ci] 2026-10-27 18:22:08 +00:00
takuto-NA d245541075 Reuse placement-independent model results (#6525) (#6527)
* claim: #6525

* perf(eval): reuse local geometry across placement changes

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

* fix(engine): preserve recovery journal through shared refresh

The shared evaluator retains the document Arc, so refresh and body-name
pinning can copy it after command_done recorded its address. The next read
then mistakes that copy for an outside edit and discards the replay.

Record completion after refresh, name pinning or failed-command restoration.
Add a minimal journaled box/inspect regression in the existing crash suite.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:51:09 +00:00
Mateusz Sych 9a7b657fdc Assistant: connect the last-used agent on startup (#7009) (#7124)
* claim: #7009

* Assistant: connect the last-used agent and open the panel at start (#7009)

Two Settings > AI options, off by default: when SolidCraft starts, connect
the agent started last (once the installed agents are known; a missing one
is not swapped for another) and open the Assistant panel. Not for runs
started with --control.

A connection that never gets ready (no agent, start error, the agent ends
while starting) is a new Failed state: the header reads Connection failed
and Connect reads Retry. The control channel reports it as "failed" and
takes connect_at_start / open_at_start.

* Assistant: keep the start-up agent when a design replaces the blank one (#7009)

Opening a design over the blank design SolidCraft starts with reset its
chat and stopped the agent connected at start. A chat not asked anything
yet now starts over in place and keeps its agent. Turning AI features
off also drops a start-up connection still waiting for discovery.

* i18n: Arabic, Hebrew and Vietnamese for the start-up options (#7009)

ar, he and vi are complete catalogs on main now, so every new English
message needs them.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-27 16:38:26 +00:00
tr1pp4 d0c594d311 History: a face picked in the view lights the feature that made it (#6910)
Hovering or picking a face in the view lights the feature that made that face (its
creator, as Edit Feature on a face finds it: the hole, not the plate it was cut into)
in the bottom timeline, the History on the left and the Browser's Features folder;
edges and vertices light the feature that made their body, as before. A new face or
edge pick scrolls the history shown to that feature once, through the Find in
Timeline reveal.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:25:28 +00:00
Parman Mohammadalizadeh 4515af3eb9 Update Links reads again only the linked designs that changed (#5606) (#5674)
* Update Links reads again only the linked designs that changed (#5606)

Update Links read, evaluated and STEP round-tripped every linked design on every run. Remember a digest of each linked file, of the STEP it gave and of its face names, and skip a link whose file, bodies and face names are all unchanged. Read a linked design through its saved model when the file has one, as opening it does.

* Drop an import the rebase left unused

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-27 16:01:57 +00:00
github-actions[bot] c5fbf6650c Regenerate generated files and fold pending files [skip ci] 2026-10-27 17:09:41 +00:00
github-actions[bot] 1aaa2cdc2a Regenerate generated files and fold pending files [skip ci] 2026-10-27 17:03:03 +00:00
semmelrinde 98b57a3588 Report a bug: Retake rebuilds the details instead of refusing, and keeps the screenshot (#7350) (#7352)
* claim: #7350

* Report a bug: Retake rebuilds the details instead of refusing, and keeps the screenshot (#7350)

Once anything changed with a report open, every Retake refused with "Settings or model
changed. Close this draft…", and each refused, waiting, timed-out or stale capture deleted the
screenshot the draft had. Retake now rebuilds the technical details for the current view
(keeping what was typed, edited details and a crash draft's body) and re-baselines before it
captures; the screenshot is replaced only when a new frame is accepted.

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

* Locales: Arabic and Hebrew for the new strings

ar and he are complete catalogs on main now (#3458): every new message
needs them. The pending-messages test compares right-to-left messages in
display order once #7532 lands (#7527).

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

* Locales: Vietnamese for the new strings

Vietnamese became a complete catalog on main (#5823), so every new English
message needs it too.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:17:11 +00:00
bherbruck 35913ced46 Fix flaky dielectric_preflight_and_start test: render a view that does not hit a box edge (#7722)
The default view sends the single pixel's ray through a box edge, which the
raytracer rejects as an ambiguous interface normal. The job then ended Failed
whenever the render thread got ahead of render.cancel (about 7% of runs under
load), and the test only accepted Cancelled or Completed. Render off-axis so
the outcome no longer depends on that race, and print the phase and error if
it ever fails again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:16:48 +00:00
semmelrinde 02981d6bcf Lost GPU: a dropped frame no longer crashes the window's close (#7148) (#7400)
* claim: #7148

* Lost GPU: a dropped frame no longer crashes the window's close (#7148)

On a lost device wgpu's present and discard return early and leave the
acquired frame's image in the surface, holding a swapchain acquire
semaphore. Dropping the surface later (the window closing for the
restart after the loss) then panicked in wgpu-hal. The painter now holds
the frame as AcquiredFrame, which destroys a second handle to the
frame's texture after the frame is presented or dropped; wgpu-core's
destroy of a surface texture takes the image back without checking the
device, and does nothing once a present or discard took it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:48:04 +00:00
amolinari-autotex 96b3ebba54 Physical Material: initialize from selected bodies (#7240) (#7241)
* Fix physical material dialog selection initialization

* Fix material selector translations for complete catalogs
2026-10-27 14:40:47 +00:00
semmelrinde 5bd8387cf7 Repeat Last Command in the command palette, with a key you can change (#7077) (#7353)
* claim: #7077

* Repeat Last Command in command search, with a key you can change (#7077)

Shift+R already started the last tool or dialog again (#3045), but it was a
key the app kept for itself: command search could not find it and Keyboard
Shortcuts could not change it. It is now the command edit.repeat, with
Shift+R as its default key. A call through the engine is refused, because
only the app has a tool to start.

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

* Locales: Arabic and Hebrew for the new strings

ar and he are complete catalogs on main now (#3458): every new message
needs them. The pending-messages test compares right-to-left messages in
display order once #7532 lands (#7527).

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

* Locales: Vietnamese for the new strings

Vietnamese became a complete catalog on main (#5823), so every new English
message needs it too.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:40:36 +00:00
semmelrinde ec877315c3 Sketch: add points to an existing spline (#6533) (#7384)
* claim: #6533

* Sketch: add points to an existing spline (#6533)

A spline could not take more points after it was drawn. The new command
sketch.spline.insert_point inserts a fit point on the curve where it
passes nearest a spot, between its neighbours, or adds a new first or
last point. Existing points keep their indices, so constraints stay. A
right-click on a fit point spline offers Add Spline Point at the clicked
spot. Control point splines are refused for now: with uniform knots one
more control point would reshape the whole curve.

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

* Locales: Arabic and Hebrew for Add Spline Point

ar and he are complete catalogs on main now (#3458). The pending-messages
test compares right-to-left messages in display order once #7532 lands
(#7527).

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

* Locales: Vietnamese for the new strings

Vietnamese became a complete catalog on main (#5823), so every new English
message needs it too.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:24:13 +00:00
github-actions[bot] 98d021c523 Regenerate generated files and fold pending files [skip ci] 2026-10-27 16:34:22 +00:00
tr1pp4 04e4e72ea1 History: delete a folder with its features, and suppress a whole folder (#7219) (#7220)
* claim: #7219

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

* History: delete a folder with its features, and suppress a whole folder (#7219)

`timeline.delete` and `timeline.suppress` take `groups: [timeline folder ids]`:
every feature in those folders and their nested folders, in one undo step.
A delete takes the folders themselves too; `timeline.ungroup` still keeps the
features. The History on the left offers Suppress/Unsuppress Selected Folders
and Delete Folders and Features… (asking first, like Delete Selected
Features…, when something outside needs them); a folder whose features are
all suppressed is drawn faint.

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

* History folder actions: Arabic, Hebrew and Vietnamese strings (complete catalogs)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:28:16 +00:00
Mateusz Sych 2151bc0497 Drive Joints: say why a rigid joint can't be driven and offer Edit Joint (#6981) (#7051)
* claim: #6981

* Drive Joints: say why a rigid joint can't be driven and offer Edit Joint (#6981)

Drive Joints opened on joint 0 when no joint moved, and its preview ran
joint.drive on that rigid joint, so the dialog showed a red "a rigid joint
does not move" with a Create Issue button.

The dialog now says a rigid joint holds its parts still and how to let them
move (Revolute to rotate, Slider to slide, Cylindrical for both), with an
Edit Joint button that turns it into Edit Joint on that joint. Its commands
refuse a rigid joint, so nothing is previewed and OK stays off. A rigid
joint picked in the browser opens Drive Joints on it to say the same; the
joint box lists only joints that move. joint.drive's own refusal names the
types that move and joint.edit.

* Drive Joints: the rigid joint message in Arabic, Hebrew and Vietnamese (#6981)

Main now delivers ar, he and vi complete, so every new message needs them.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-27 13:20:35 +00:00
DuBearte 1a9c4cd99d Sketch: a circumscribed polygon stays regular when dragged (#7395) (#7438)
* claim: #7395

* Sketch: a circumscribed polygon stays regular when dragged (#7395)

Circumscribed Polygon held its sides equal and tangent to the support
circle only. Equal sides round one circle leave every other corner free
(a rhombus round a circle), so a hexagon had 9 degrees of freedom instead
of 4, and dragging its centre or a corner pulled it out of shape (radius
10 became 8.02, angles 174/66 degrees).

Its corners now also lie on a construction circle concentric with the
support circle, which makes it regular as an inscribed polygon is, and
one side is tangent to the support circle, which sets its size: dof 4
(centre, size, turn). One tangent rather than all of them, so no
constraint is redundant.

Test in sketch_ux_tests: 4, 5 and 6 sides, dragged by the centre and by
a corner, stay regular with their sides on the support circle.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:47:35 +00:00
Colin Faulkingham b1cf66b628 Fix stored world-axis inputs for revolve (#7179) (#7406) 2026-10-27 12:45:40 +00:00
NOWN-DEV 285fa9433b Sketch: a distance along a line keeps its side (#7506) (#7510)
A point a distance from another point on the same line has two places on it, the same distance
either way. A solve after a value edit or a link move could land on the other one and report the
sketch solved, with no warning: the point jumped to the other side of its anchor, twice the
distance away. Such a distance is now an orientation the solves keep, like an unsigned offset's
side: which way along the line the one point lies from the other. One point must be on the line
by a constraint (an end, a point held on it, or coincident with either); the other may lie on it
by what else holds it (the mirror image of a point on it across a line square to it), read where
it is in the reference state.

Test: a point 3 mm along a linked rail from a point held on it, or mirrored onto it: the image
back past that point counts as a flip, and the rail walked far keeps the side.

Fixes #7506.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:43:05 +00:00
sascha2101 f5bfd6d1f2 Sheet metal: a base or contour flange hides its sketch (#6804) (#7151)
* claim: #6804

* Sheet metal: a base or contour flange hides its sketch (#6804)

Only an Extrude or a Revolve counted as using its sketch, so the sketch of a
base flange stayed shown. Its line on the plate's bottom edge lies exactly on
that edge and took the Flange dialog's click: a sketch line makes a contour
flange, a separate flat wall with no bend whose Flip moves it into the plate.

A base or contour flange now uses its sketch as an Extrude does: hidden unless
shown on its own, and shown while its feature is edited.

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

* Sheet metal: test that a click on the bottom edge makes an edge flange (#6804)

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:43:00 +00:00
bherbruck 48c8965853 CI: run the GPU path tests only for render changes, plus nightly (#7742)
The merge queue ran the four-shard GPU path tracer tests (about 56 runner-minutes)
on every group. Now they run when the diff touches render code (crates/render,
crates/gpu/src/path, the render workspace UI, assets/render), or a pull request
carries the new render-tests label (a pull request or a group holding one). The
nightly and manual runs still always run them; a push to main does not.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:58:30 -04:00
github-actions[bot] e7e8a1ef26 Regenerate generated files and fold pending files [skip ci] 2026-10-27 15:37:21 +00:00
Matthew Knutson ae530bff44 Typed contracts for the cylinder, sphere, torus, cone, pipe, bounding solid and scale (#2829) (#7145)
* Typed contracts for the cylinder, sphere, torus, pipe, bounding solid and scale

Contracts for solid.cylinder, solid.sphere, solid.torus, solid.pipe, solid.bounding_solid and
solid.scale, with `FeatureResult` output, and the shared types `AxisDir` (calling
`cmd::axis_dir_form`, split from `axis_dir_param`), `NumberExpr` and `ScaleFactors`. The
descriptions say what the evaluation does: the limits it enforces, the real default scope of
`targets`, and where `participants` overrides a key or has no effect. Scale makes no body, so it
declares `name` and `participants` itself. Box's descriptions get the same corrections. The
reply test runs every form in Warn and checks for no `param_warnings`; the guide lists the new
types and adds lessons from the conversion.

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

* Typed contract for the cone

`solid.cone`, added on main beside the primitives this PR converts, gets its contract and
`FeatureResult` output: the cylinder's keys plus `top_radius` / `top_diameter`, with the
evaluation's limits in the descriptions (a radius above 0, a top radius of 0 or more, a height
that is not 0 and may be negative, sizes within 1e7 mm). A `null` top radius stays the handler's
to refuse, since the handler takes it beside a `top_diameter`.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:42:16 +00:00
yhamil04 3574956d29 Graphics: the window no longer waits for Direct3D 12 shader compiles (#4860) (#7288)
* claim: #4860

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

* Graphics: the window no longer waits for Direct3D 12 shader compiles (#4860)

On Direct3D 12, wgpu compiles every pipeline's shaders with FXC when the
pipeline is made, and nothing is kept between launches, so the window
waited seconds for the viewport.

- The GPU tessellation shader, most of that time, compiles on a thread of
  its own. Bodies are built on the CPU until it is ready, then drawn from
  their plans; where it never compiles they stay on the CPU, as before.
- The viewport's pipelines build in parallel, one job per fragment shader
  (solidcraft_gpu::concurrent). A job that errs, panics or cannot start
  runs again on the calling thread, so errors reach the caller's scopes.
  Threads only on Vulkan, Metal and D3D12 with FXC.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:41:55 +00:00
semmelrinde 27728b10e2 Trackpad: inertia stops when Shift changes mid-orbit (#7327) (#7361)
* claim: #7327

* Trackpad: inertia stops when Shift changes mid-orbit (#7327)

macOS sends the inertia after the fingers lift as its own scroll bracket,
with the modifiers held at the time. Each move was routed by its own
modifiers, so releasing Shift while an orbit coasted turned the rest of
the inertia into a pan (and pressing Shift turned a coasting pan into an
orbit). A bracket that starts as the fingers' one ends now keeps the
fingers' mode and ends when the modifiers change. Switching between pan
and orbit while the fingers are down works as before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:41:54 +00:00
JK3D 1c109be836 Headless pictures: no highlight left over from the picture before (#7225) (#7326)
* claim: #7225

* fix: headless pictures no longer keep the previous picture's highlight

Capturer::rgba_highlighted left the highlight slot empty when a picture had
no highlight. The renderer keeps its last highlight scene while the slot is
empty, so a selected edge's band drawn for one picture showed in the next
one on the same capturer. The depth tests share their capturers across test
threads, so they failed at random: an edge "192 of 612 px drawn", a washer
showing through its plate, a cut showing what lies behind it. A picture now
always gets a highlight scene of its own, empty without one.

Clearing the leak showed that a_selected_edge_on_a_grazing_face_draws_whole
passed at 88 degrees in a design of radius 30 only by counting the leaked
band: its reference line lies 143 mm behind the sheet's middle, past that
design's far plane, and drew nothing. The small design now holds the edge.

New test: a_picture_without_a_highlight_shows_none_of_the_one_before.
Full `--test depth` runs on Apple silicon: 10 of 10 failed before, 0 of 20
after.
2026-10-27 12:41:48 +00:00
JK3D 388266ca37 Rename: an imported body with numbered siblings is renamed without evaluating (#7274) (#7277)
* claim: #7274

* Renaming an imported body with numbered siblings evaluates nothing again

Since #2467, Model::rename_body evaluates the design whenever another body's
name is worked out from the renamed one's (`X (2)`). Every STEP import whose
solids share a label has such names (`Part`, `Part (2)`, ...), so renaming
one of its bodies re-read the import and meshed every body again.

body.rename keeps the names of a body's siblings and of later bodies named
after it, so for a body no later feature changes (the same mesh from the
feature that made it to the end), renaming it in the model gives the names
evaluating the renamed design gives. That case is followed again, as before
#2467; bodies later features change still take #2467's checks.
2026-10-27 12:41:39 +00:00
semmelrinde 2346c1ae81 Web app: Report a bug saves its screenshot as a download (#7192) (#7197)
* claim: #7192

* Web app: Report a bug saves its screenshot as a download (#7192)

Save screenshot… wrote the PNG with image's save, that is std::fs, which
the browser doesn't have. The screenshot is now encoded in memory and
written through vfs, as every other save is, so the web app hands it out
as a download. Natively the same file is written and ui.screenshot
replies as before. A failed save says why in plain text instead of a
Rust debug value.

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

* Web app: a screenshot without a path no longer panics (#7192)

`ui.screenshot` without a `path` saved to `std::env::temp_dir()`, which panics on
wasm32-unknown-unknown (std has no file system there), and the web app's control
requests come through `window.solidcraft.request`. `vfs::temp_dir()` now returns
`vfs::BROWSER_TEMP`, a folder of the in-memory files, in the browser, and
`save_screenshot` and `vfs::design_folder_refusal` (reached when a request names
`allow_paths` while a design is open) use it. Natively nothing changes.

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

* Web app: keymap and theme exports download (#7192)

Both exports wrote with `std::fs::write`, which always fails in the browser. They
now write through `vfs::write` like the other saves, so the web app hands the file
out as a download. Natively the file is the same; an agent's folder scope now
applies to them as to every other write.

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

* Tests: say what the folder-scope checks of the saves show (#7192)

The screenshot test wraps the write in a folder scope by hand: that proves the write goes through vfs, not that ui.screenshot's write runs inside the request's scope. ui.screenshot checks the agent's folders when the request arrives; the PNG is written in a later frame, outside that scope, as on main.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:41:21 +00:00
raykholo 8372c476ab Add the built-in CGA theme (#7593) (#7594) 2026-10-27 12:41:08 +00:00
Liu XingJian 4ad285570c Control channel: retire a subscription when the events thread ends (#7226) (#7336)
The seat of a subscribed connection on the DesignWatch lived in the
connection's serve thread. It left only when that thread saw the end of
the stream, so one unbounded blocking read stood between a closed client
and the count. Under load (GPU backend probing and crash-dump children
on Windows) that read can sit for seconds; the test's 10 s deadline then
expired and `!WATCH.subscribed()` failed (#7226).

Move the seat onto the events thread. The thread ends on `stop`, or on
the first write a closed client refuses, so the next publish retires the
seat. The test now publishes while it waits, and its wait no longer
depends on the serve thread.
2026-10-27 12:40:41 +00:00
Kai Evans 4766aea8d7 Themes: Okibi's and Akari's start page rail is the void, not the ember (#7061)
Neither theme set the File view's rail colours, so SolidCraft derived the rail from the accent and
the start page's whole sidebar was the primary orange, against the design system's one-or-two
ember budget (#4868 derives an unset rail from the accent). The rail is now n0 (void, beside the
app bar), its hovered and open row n3 (raised), and its text n11.
2026-10-27 12:40:40 +00:00
semmelrinde ad749d3399 Joints: the model is evaluated for the placements a solve leaves (#7084) (#7085)
Session::refresh evaluated the model and then solved the joints, which
writes the solved placements into the design. A feature that reads where
a part is placed kept the result made for the placement before the
solve: a mirror of a jointed part's body, made from the root, ended up
4 mm below the part after a move the slider took back.

When the solve moves parts, the model is now evaluated again for the
placements the design holds, and the joints solved for that model, at
most three rounds. Features that don't read placements come from the
cache.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:40:29 +00:00
NOWN-DEV 0f8593b180 Sketch: a tangent at an end the line and the round share keeps its side (#7428) (#7447)
A tangency at a point the two curves share (or a line's end held on the round) is solved as a
block of its own, and the orientations a solve keeps left it out. When a point placed by
parameters moved far, the line could land on the other tangent from its far end (its mirror
image) with every constraint met and no warning. It now keeps the round's side of the line, as a
tangency away from the ends already does: kept where the move can be walked there, reported as
a flip where it can't.

Fixes #7428.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:40:26 +00:00
Dahak Builds f8d2d90a16 Sketch: dimension placement and editing (#44) (#7056)
* claim: #44

* Sketch: step dimension texts clear of each other and keep placed offsets (#44)

* chore: re-trigger checks (Duplicate PR check was canceled by runner)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-27 12:40:19 +00:00
github-actions[bot] 69a46fb6a3 Regenerate generated files and fold pending files [skip ci] 2026-10-27 15:08:50 +00:00
github-actions[bot] b2f69ab314 Regenerate generated files and fold pending files [skip ci] 2026-10-27 15:00:37 +00:00
sascha2101 7e8ccad341 Browser: a body's name hides the body, not a sketch of the same name (#7206) (#7604)
* claim: #7206

* Browser: a body's name hides the body, not a sketch of the same name (#7206)

`browser.visibility` looked a key up as a feature first, so when a sketch
had exactly the name of a body (a sketch named after the body it makes),
the body's eye in the browser hid and showed the sketch instead, and the
body could not be hidden at all. The browser names a sketch by its id and
a body only by its name, so a name that is exactly both now names the
body. The sketch still hides and shows by its id; matches that differ
only in case are left as they are (#6967, #7004).

Fixes #7569 too.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:40:02 +00:00
P'tite Tête d28874bec1 French: complete the UI catalog (#7131) (#7133)
* claim: #7131

* feat: Complete the French UI catalog (#7131)

- Add the 1,153 messages missing from fr.json as a pending fr block
- Keep the terminology already used in fr.json and French typography

---------

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-27 12:39:53 +00:00
semmelrinde 95515ed2bf Report a bug: take the screenshot once the preview is done instead of giving up (#7171) (#7173)
* claim: #7171

* Report a bug: take the screenshot once the preview is done instead of giving up (#7171)

A report opened while a preview was being computed took no screenshot
and asked to be reopened, which loses what was typed. It now waits for
the preview with the report open and takes the screenshot as soon as it
is done; after 10 s it shows the preview unfinished and says so in the
report (a hanging preview is often what is being reported).

The preview the report waited for belongs to its context unless the
design, the dialog or the selection changed meanwhile, so the retake
check compares those alone. What is typed while the capture hides the
report waits for it, and its focused field gets the focus back.

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

* Report a bug: keep the new helpers apart from the other report changes

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

* Report a bug: keep what is typed as the waited-for screenshot is taken

The screenshot a report waits for starts in the frame the preview
finishes, before the form is drawn, so what was typed in that frame went
nowhere. It is now held with what comes while the form is hidden. The
report's context also leaves the dialog's own state out: a preview error
the dialog shows meanwhile (a sketch fillet's) no longer counts as a
change that refuses the screenshot, since it is often what is reported.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:39:32 +00:00
Victor Santos Oliveira 46552bdc3a Profiles: explain public selection forms in parameter errors (#7180) (#7491)
* claim: #7180 public profile selection diagnostics

* fix(engine): explain public profile selection forms
2026-10-27 12:39:31 +00:00
semmelrinde ae3d4faf53 macOS menu: disable design commands behind an open Report a bug draft (#7188) (#7193)
* claim: #7188

* macOS menu: disable design commands behind an open Report a bug draft (#7188)

The native menu bar kept running its actions while a Report a bug draft was
open, although the app's own toolbar and shortcuts wait behind it: Edit > Undo
or Cmd+Z removed the last feature, so the report described a design that no
longer existed. FileAction::enabled now refuses every action but Quit while a
draft is open. Settings, Keyboard Shortcuts and Agent Chat are refused too:
their windows open over the report but take no input until it closes. The
menu's Undo and Redo still go to a focused report field
(FileAction::menu_enabled), which no longer leans on Settings being enabled.

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

* Menus: Quit and Help's windows wait behind an open Report a bug draft

Quit switched to the first design with unsaved changes under the draft and
asked about it in a prompt that can't be answered while the report blocks
the app; with nothing unsaved it quit and lost the typed report. Like any
modal window, the draft now turns Quit off. Help's items that open a
window (About, Keyboard Shortcuts, Messages, Crash Reports, Connect AI
assistants) wait too, in the native menu and in the app's Help menu;
Documentation and Report an Issue… stay.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:39:27 +00:00
semmelrinde 05eb283242 Report a bug: focus the empty description once, not every frame (#7190) (#7195)
* claim: #7190

* Report a bug: focus the empty description once, not every frame (#7190)

After Continue on GitHub found What happened? empty, the form moved the
focus back to it on every frame, so typing in Summary went into the
description. Continue now re-arms the one-time focus (and scroll into
view) instead; the other fields stay free to type in.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:39:17 +00:00
yhamil04 6b7f1934b1 Startup tests: start the backends the app would, no OpenGL driver on Windows (#1156) (#7217)
* claim: #1156

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

* Startup tests: start the backends the app would, no OpenGL driver on Windows (#1156)

The software-adapter test enumerated every wgpu backend and the probe test
asked OpenGL on its own even where the app stops at D3D12, so both loaded the
GPU vendor's OpenGL driver (atio6axx.dll on AMD) into the test process, where
it once aborted the process at exit with c0000409. The adapter test now starts
what the software renderer starts (D3D12 alone on Windows) and the probe test
probes in the app's order, stopping at the first backend with a GPU.

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

* Startup tests: read the software renderer's backends from wgpu_options

Review: the adapter test now takes the backends from the app's own
wgpu_options(true, None) instead of a copy of its choice, and the probe test
no longer re-checks the order choose_backends' own test covers.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:39:01 +00:00
Badger B f9c8abb329 Add German translations, batch 1 (#7237) (#7260)
* Add German translations, batch 1 (#7237)

* Update translation for 'Import material into design'
2026-10-27 12:38:47 +00:00
CamWight 53cba0cf51 AGENTS.md: drop the leftover merge-conflict marker (#7279) (#7282)
The last line was a `>>>>>>>` marker left from resolving a conflict in #6444.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:38:38 +00:00
CamWight d95646378b cargo xtask skills: read SKILL.md front matter on a CRLF checkout (#7281) (#7284)
* claim: #7281

* cargo xtask skills: check the files as Git stores them, on a CRLF checkout too (#7281)

A Windows checkout with core.autocrlf gives AGENTS.md and the SKILL.md files CRLF line endings,
so `front_matter` found no `---\n` and every skill was reported, and each line's `\r` counted
towards the size caps. The check now reads the files with CRLF taken as LF, as #4960 did for
icon_audit. The new test checks a CRLF skill filled to the cap.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:38:34 +00:00
phanoob 75c8700724 Sketch: a circle centred on an existing point shows its Coincident glyph (#7299) (#7331)
* claim: #7299

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

* Sketch: a circle centred on an existing point shows its Coincident glyph (#7299)

The centre circle tool sent a centre snapped to an existing point as that
point's id, so the circle shared it: attached, but with no constraint and so
no glyph to see, pick or delete. A plain point snap now goes as its place,
where `connect` ties the new centre to the point with a Coincident the sketch
draws. Midpoint, on-curve, crossing and body-vertex snaps still go as refs,
so the centre stays held there (#3099 keeps passing).

Fixes #7299

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-27 12:38:23 +00:00
phanoob fac811565b Sketch: an arc centred on an existing point shows its Coincident glyph (Refs #7299) (#7346)
* Claim: centre arc on an existing point shows its Coincident glyph (Refs #7299)

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

* Sketch: an arc centred on an existing point shows its Coincident glyph (Refs #7299)

The Centre arc tool sent a centre snapped to an existing point as that point's id, so
the arc shared it with no constraint and the sketch drew no glyph. A plain point snap
now goes as its place, where draw's connect ties the new centre with a Coincident;
midpoint, on-curve, crossing and body-vertex snaps still go as refs.

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-27 12:38:15 +00:00
semmelrinde fb7ca86dc7 Highlight the selected sketch in the viewport (#7156) (#7347)
* claim: #7156

* Highlight the selected sketch in the viewport (#7156)

Picking a sketch in the Browser or the timeline selected it, but the
viewport lit nothing: the highlight only drew the bodies and faces a
feature made. It now draws all of a picked sketch's curves in the
selection colour, over the model and also while the sketch is hidden,
so a sketch is easy to find without hiding and showing it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:38:11 +00:00
semmelrinde 9b84789da0 Report a bug: include the Messages log and other failing features (#7348) (#7349)
* claim: #7348

* Report a bug: include the Messages log and other failing features (#7348)

About half of the reports say "No command error was recorded" and often
can't be reproduced. The error was often in the Messages log, or on a
feature that was neither selected nor the latest. The technical details
now list every failing feature (id, name, kind, error; at most 10) and the
last 8 messages with level and age. Each entry is one quoted line of at
most 300 characters, with the home folder shown as ~.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:38:07 +00:00
semmelrinde 9d01dcb358 Open from computer: let iOS and iPadOS pick .solidcraft files (#6977) (#7351)
* claim: #6977

* Open from computer: let iOS and iPadOS pick .solidcraft files (#6977)

The iOS file picker matches an input's accept list by system file type,
not by extension. No system type has the .solidcraft extension, so the
designs were greyed out. On an iPhone or iPad the file inputs now offer
every file; other devices keep the extension filter.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:38:02 +00:00
semmelrinde 0b0886b87c A surface left without a configuration is configured again, not a crash (#7007) (#7401)
* claim: #7007

* A surface left without a configuration is configured again, not a crash (#7007)

wgpu reports device-loss errors to no error scope. On D3D12 a driver reset
found by Surface::configure drops the old configuration and reports nothing,
so the surface was taken as configured and the next acquire panicked with
"Surface is not configured for presentation". A configure that meets a lost
device or an unsupported surface is now a refused configure, the acquire runs
under an error scope, and its Validation status reconfigures the surface.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:37:31 +00:00
Jackson Oursland 6081226fa6 Test: the render-guides picture test keeps its evidence when it fails (#7440)
model_render_follows_the_grid_and_origin_switches_unless_asked compares nine
renders byte for byte and has failed once in a full run without coming back
alone. A mismatch now names the step, counts the differing pixels, lists the
renderer that drew each picture and keeps both PNGs.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 12:37:16 +00:00
Kai Evans 329ef29f24 Value fields on the canvas grow with an expression typed into them (#7470)
The value box beside a command's geometry and the placement fields kept a number's width, so a
formula or parameter names scrolled out of sight while typing. They keep that width for a number
and grow with the text, up to three times it.
2026-10-27 12:37:08 +00:00
semmelrinde f82e517bcb Toolbar: a panel with no buttons keeps one that opens its menu (#7166) (#7503)
* claim: #7166

* Toolbar: a panel with no buttons keeps one that opens its menu (#7166)

Removing New Sketch from the Model tab (Remove from Toolbar, or a drag off
the toolbar) left the Sketch panel empty above its label, so Sketch seemed
to be missing from the toolbar. A panel whose buttons are all gone is now
drawn like a collapsed panel: its first command's icon over a caret, which
opens the panel's drop-down, in the same room.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:36:57 +00:00
Colin Faulkingham e8302b5757 Sketch: delete dimensions from the right-click menu (#7155) (#7170)
* claim: #7155

* fix(ui): delete sketch dimensions from the context menu
2026-10-27 12:36:55 +00:00
Lars Jansen 98762fd20b i18n test: compare pending right-to-left messages in display order (#7527) (#7532)
* claim: #7527

* fix(i18n): compare pending right-to-left messages in display order (#7527)
2026-10-27 12:36:46 +00:00
semmelrinde e0809b427c Web app: Import… reads shortcuts and theme files through vfs (#7643) (#7644)
* claim: #7643

* Web app: Import… reads shortcuts and theme files through vfs (#7643)

Import… on the Keyboard Shortcuts page and the theme strip's Import… tile
read the picked file with std::fs::read_to_string. In the browser a picked
file is kept in vfs's in-memory files under /upload/, and std::fs on
wasm32 is unsupported, so every import failed. Both now read through
vfs::read_to_string, capped (a theme file at theme_files::MAX_BYTES, a
shortcuts file at the new toolbar_custom::MAX_IMPORT_BYTES), like the
other imports. Natively it is the same file on the disk.

The new tests import a file outside a vfs folder scope (refused, nothing
imported) and inside it (imported). With std::fs both fail: the file
outside the scope is imported.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:36:25 +00:00
Kai Evans 17339cf65c Redefine: the features after the edited one are computed once (#7076)
timeline.redefine evaluated every later feature in its trial session, then again in the session:
the evaluation cache keeps only the last few states of a design with large bodies, so the trial's
results were gone by then. The trial keeps them in the session's kept results (or its own), and
the session's evaluation takes them from there. EvalCache::with_budget gives tests a small cache.
2026-10-27 12:35:32 +00:00
Colin Faulkingham 22ede1bde3 Mouse gestures: keep the tool search open while editing (#7121) (#7132)
* claim: #7121

* Fix mouse gesture tool-search focus
2026-10-27 12:33:33 +00:00
sascha2101 7895c14e3f Preferences: the Colour vision preview shows the chosen mode again (#7152) (#7169)
* claim: #7152

* Preferences: the Colour vision preview shows the chosen mode again (#7152)

Since the preferences redesign the Colour vision section previewed a mode
with four status chips (Selected, Warning, Error, Done). No colour vision
mode changes those colours, so the chips looked the same in every mode.

The section shows the sample of what the modes recolour again (sketch
lines, axes, bends, Draft Analysis, a failed feature), drawn in the mode
being chosen (Tokens::get_in). The status chips stay as they are.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:29:55 +00:00
semmelrinde d9ccba0134 Report a bug: scale a large window's screenshot down instead of refusing it (#7091) (#7092)
* claim: #7091

* Report a bug: scale a large window's screenshot down instead of refusing it (#7091)

A frame over 16 million pixels (a 6K display, a window across two 4K
screens) was refused with "Screenshot dimensions are unsupported", so the
report went out without its picture. Such a frame is now averaged down by
the smallest whole factor that fits.

The frame is also fitted to the longest side a texture can have: a frame
under the pixel limit but wider than the GPU's limit (three ultrawide
screens, 10320 x 1440, on a device limited to 8192) made the preview's
upload fail wgpu validation, and the window's device panics on that.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:29:09 +00:00
bherbruck dc32cff620 Release 0.24.1: version bump, design format 40 (#7653)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:34:21 -04:00
dudedagger42 531fc04db8 Inspect: center of mass measures the world state, as Measure does (#5246) (#5268)
* Inspect: center of mass measures the world state, as Measure does (#5246)

inspect.center_of_mass read the model state, so in an assembly a moved
occurrence was measured where its bodies were made, a second placement
was left out, and a placed copy's world name was "no such bodies". It
now reads the world state, as inspect.measure does, and agrees with
component.properties' mass centre at uniform density.

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

* Center of mass test: unground the placed part before moving it

Upstream now grounds a design's first part when it is made.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:04:53 +00:00
Nathan Dane 4907429439 Render workspace: original icon drafts for owner review (#6281) (#6283)
* claim: #6281

* assets: add Render appearance, library and paint icon drafts for owner review

* assets: add Render lighting, environment and light-type icon drafts for owner review

* assets: add Render camera, shot, frame and strip icon drafts for owner review

* assets: add Render output, result and compare icon drafts for owner review

* assets: list the Render icon drafts in the inventory and ATTRIBUTION

* assets: remake the Render light icons as one family with a shared dotted stem

* assets: remake the Render look icons around the lit look sphere

* assets: redraw the Render look icons so each action reads at a glance

* assets: record the owner's acceptance of the Render light icons

* assets: simplify the Render look icons and make Remove Override an alias of From Material

* assets: record the owner's acceptance of Render look icons

* assets: mark the removed look on From Material with a cross instead of an arrow

* assets: record the owner's acceptance of further Render icons

* assets: redraw the Render library, presets, environment, ground and backdrop icons

* assets: restore the first Render library and backdrop drawings and equalise the cube in the studio icons

* assets: record the owner's acceptance of the Render studio icons

* assets: redraw the Render camera and shot icons as one family

* assets: redraw the Render All Shots and Lens icons so they read at a glance

* assets: put the play badge back on the Render All Shots icon

* assets: redraw the Render output and result icons around one picture base

* assets: give Render Image its own play badge and enlarge the Compare handle

* assets: record the owner's acceptance of the Render output icons

* assets: describe the final Render icon drafts in RENDER_DRAFTS.md and ATTRIBUTION.md
2026-10-27 12:02:54 +00:00
Nathan Dane 192ef78ff3 ui_kit: lit sphere thumbnail and look-chain widget (#6281) (#6284)
* claim: #6281

* ui_kit: lit sphere thumbnail, cached and bounded

* ui_kit: look-chain widget (four steps, one active, empty steps dashed)

* ui_kit: add both to the gallery and the UI kit notes
2026-10-27 11:52:55 +00:00
Nathan Dane 44a0be8d94 Render workspace UX: roadmap and claim (#6281) (#6282)
* claim: #6281

* docs: record the Render workspace interaction design and implementation roadmap

* docs: plan the Render panels so each has one purpose

* docs: replace the tabs-and-ribbon plan with the stage-bar design

* docs: drop the note about the superseded draft

* docs: plan every Render panel and where each old control goes

* docs: defer the shared lighting rig, UI steps go first
2026-10-27 11:46:54 +00:00
vandang890615 9cba66c14d i18n: catch up Vietnamese (vi) catalog with latest main (3,317 keys) (#5823) 2026-10-27 08:45:02 +00:00
dudedagger42 4afd93331f Patterns: a body of a moved component is copied where it is shown (#4763) (#4969)
* Patterns: a body of a moved component is copied where it is shown (#4763)

A pattern of bodies made outside the bodies' component (a screw's body patterned from the root)
moved each body's stored geometry, which is in its component's frame, by transforms in the
pattern's frame. The copies landed where the body sits before its component is placed: near the
origin, for every pattern kind. As Mirror does, each copy is now moved as the pattern moves the
body in the world and kept beside its source, in its component.

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

* Pattern test: unground the placed component before moving it

Upstream now grounds a design's first part when it is made.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:27:38 +00:00
github-actions[bot] 698621dfdf Regenerate generated files and fold pending files [skip ci] 2026-10-27 08:19:30 +00:00
Lars Jansen 5fc23436ca History: show the origin and bodies at the top (#6490) (#6495)
* claim: #6490

* feat(history): show the origin and bodies at the top (#6490)
2026-10-27 07:46:25 +00:00
Jackson Oursland 76879f8b39 Viewport: write hover edges as one record per line, colours interned once (#7416)
Hovering many bodies re-emits every edge in the highlight's colours. Each edge
interned its three colours into the palette again, each segment was written in
eight small appends, and segments without their faces' normals encoded the
same zero normals again. Lines are now one record, the edge colours are
interned once per highlight, the buffers are reserved per edge, and the shared
width word is worked out once.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 07:46:19 +00:00
Jackson Oursland 514b0c84f9 Viewport: hold the blocks the sorted glass stream was taken from (#6970)
The no-OIT glass cache compared the blocks' vertex and run buffers by address
without holding them, so a freed buffer's address could come back for other
contents. It now holds them and compares with Arc::ptr_eq.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 06:35:15 +00:00
Jackson Oursland ddca1054ab Viewport: leave bodies out of the view out of their blocks' draws (#7436)
Instanced placements were culled per view, but a block drew every body in it
whatever the camera saw. Blocks now keep each body's vertex span and box; when
the view changes, each class's runs are drawn without the bodies whose boxes
are out of the view. A block whose bodies are all in view draws its runs as
they are.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 06:35:11 +00:00
3845 changed files with 146207 additions and 275449 deletions
+8 -3
View File
@@ -1,9 +1,14 @@
# cargo-mutants (docs/mutation-testing.md). Mutants are judged by the black-box suites that will
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test).
# The kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test)
# and the pull-request sample of the robustness goldens (every 8th seed, as ci.yml). The
# kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
# backend-neutral suites would miss when the kernel is swapped.
profile = "mutants"
additional_cargo_test_args = [
"--test", "oracle_cases", "--", "--include-ignored",
"--test", "oracle_cases", "--lib", "--", "--include-ignored",
"oracle_suite",
"robustness_goldens::seed_00", "robustness_goldens::seed_08", "robustness_goldens::seed_16",
"robustness_goldens::seed_24", "robustness_goldens::seed_32", "robustness_goldens::seed_40",
"robustness_goldens::seed_48", "robustness_goldens::seed_56", "robustness_goldens::seed_64",
"robustness_goldens::seed_72", "robustness_goldens::seed_80", "robustness_goldens::seed_88",
]
+8 -5
View File
@@ -12,6 +12,7 @@ not (
(package(solidcraft-engine) & (
test(=tests::hostile_params_never_panic)
| test(/^tests::robustness::/)
| test(/^tests::robustness_goldens::seed_/)
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
@@ -43,12 +44,13 @@ threads-required = 12
priority = 100
# The slowest tests start first, so a shard does not end with one of them running alone on a
# core while the others sit idle. Medians from the CI test timings (the junit artifacts), slowest
# first; a test over ~100 s belongs here.
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
[[profile.ci.overrides]]
filter = """
(package(solidcraft-engine) & (
test(/^tests::robustness::/)
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
| test(/^tests::robustness::/)
| test(=tests::hostile_params_never_panic)
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
@@ -90,13 +92,14 @@ threads-required = "num-cpus"
priority = 100
[[profile.local-ci.overrides]]
filter = "package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic)"
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
priority = 99
[[profile.local-ci.overrides]]
filter = """
(package(solidcraft-engine) & (
test(/^tests::robustness::/)
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
| test(/^tests::robustness::/)
| test(=tests::hostile_params_never_panic)
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
+3
View File
@@ -1,3 +1,6 @@
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
.gitattributes text eol=lf
/assets/icons/ui/design/requested_icons.txt text eol=lf
# SolidCraft designs (.sc): a JSON document or a ZIP container, never Scala source, so they
# stay out of GitHub's language statistics. Git's own text/binary detection handles both kinds.
*.sc -linguist-detectable
+1 -1
View File
@@ -19,7 +19,7 @@ body:
attributes:
label: Steps to reproduce
description: |
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.sc --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
placeholder: |
1. Sketch a circle on the XY plane
+8 -7
View File
@@ -9,9 +9,10 @@ parameter to watch the timeline rebuild. [Documentation](https://docs.solidcraft
[ROADMAP](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations) lists
the known limitations. Designs from every earlier version open in this one.
**Upgrading from 0.7 or earlier.** 0.8 changed the Linux app ID to `ai.storyteller.solidcraft`:
`flatpak update` offers to replace the old app, and the `.deb`, `.rpm` and AppImage remove the old
menu entry.
**New app ID.** The app ID is now `io.tangencylabs.solidcraft` (it was `ai.storyteller.solidcraft`,
and `ai.storyteller.solvecraft` up to 0.7): `flatpak update` offers to replace the old app, the
`.deb`, `.rpm` and AppImage remove the old menu entry, and settings stay. On macOS the bundle ID
changed too, so macOS asks once more for the permissions it had granted.
**Upgrading from 0.5 or earlier.** 0.6 renamed SolveCraft to SolidCraft and made the zip
container the default save format. `.solvecraft` designs, old settings folders and
@@ -42,16 +43,16 @@ installing SolidCraft; keep both remotes in the same user installation:
```sh
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists solidcraft https://docs.solidcraft.io/flatpak/solidcraft.flatpakrepo
flatpak install --user solidcraft ai.storyteller.solidcraft
flatpak run ai.storyteller.solidcraft
flatpak install --user solidcraft io.tangencylabs.solidcraft
flatpak run io.tangencylabs.solidcraft
```
A remote added from the old `bherbruck.github.io` address no longer finds updates: run
`flatpak remote-modify --user --url=https://docs.solidcraft.io/flatpak/repo/ solidcraft`, then
`flatpak update --user`.
Flatpak installs from 0.7 or earlier use the old ID `ai.storyteller.solvecraft`: `flatpak update --user`
offers to replace it with `ai.storyteller.solidcraft`, and settings move along.
Flatpak installs under an old ID (`ai.storyteller.solidcraft`, or `ai.storyteller.solvecraft` from 0.7 or
earlier): `flatpak update --user` offers to replace it with `io.tangencylabs.solidcraft`, and settings move along.
Single-file `.flatpak` bundles include the runtime repository address; missing runtimes
still need an internet connection. [Linux installation details](https://docs.solidcraft.io/guide/getting-started.html#linux-flatpak).
+105 -89
View File
@@ -31,8 +31,6 @@
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
# The release gate (release-gate.yml) calls this workflow (workflow_call) as a Run workflow with
# cross-compilation on.
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
@@ -50,14 +48,10 @@ on:
# only (other labels are skipped, see the top of this file).
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled]
# The kernel-smlib branch (ADR 0002) runs no CI for now (owner, 2026-10-21). Drop this
# line before the branch merges back into main.
branches-ignore: [kernel-smlib]
# The merge queue: each queued group runs the full suite before it lands on main.
merge_group:
push:
# develop: the SMLib kernel's trunk until it merges into main (its PRs land there).
branches: [main, develop]
branches: [main]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
@@ -66,14 +60,6 @@ on:
description: "Also check WebAssembly and RISC-V (before a release)"
type: boolean
default: false
# The release gate (release-gate.yml): the event stays the caller's (workflow_dispatch), so a
# call runs exactly what Run workflow runs.
workflow_call:
inputs:
cross_compile:
description: "Also check WebAssembly and RISC-V (before a release)"
type: boolean
default: false
permissions:
contents: read
@@ -112,14 +98,16 @@ jobs:
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
# To ask whether the merge queue already tested a commit pushed to main.
# To ask whether the merge queue already tested a commit pushed to main, and which labels the
# pull requests of a merge group carry (the render-tests opt-in).
pull-requests: read
actions: read
outputs:
native: ${{ steps.paths.outputs.native }}
kernel: ${{ steps.paths.outputs.kernel }}
goldens: ${{ steps.paths.outputs.goldens }}
windows: ${{ steps.paths.outputs.windows }}
# The GPU path tests run: native, and the whole workspace or solidcraft-gpu selected. The
# job's `if` and the `ci` job's check both read this, so they cannot disagree.
# The GPU path tests run (see the rule where it is computed). The job's `if` and the `ci` job's check both read this, so they cannot disagree.
gpu: ${{ steps.paths.outputs.gpu }}
full: ${{ steps.paths.outputs.full }}
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
@@ -144,6 +132,7 @@ jobs:
GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
RENDER_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'render-tests') }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
APPROVED: ${{ contains(github.event.pull_request.labels.*.name, 'approved-for-ci') }}
@@ -151,7 +140,9 @@ jobs:
run: |
native=false
kernel=false
goldens=false
windows=false
gpu_paths=false
full=true
packages=
# Whether the NUL-separated paths in $1 reach any code: scripts/ci/affected.py selects
@@ -180,6 +171,29 @@ jobs:
windows=true
fi
}
# Whether the NUL-separated paths in $1 are render code: the render crate, the path
# tracer and its shaders in the GPU crate, the render workspace UI, the render assets.
# Core, kernel and CI changes do not trigger the path tests (about 56 runner-minutes):
# the tracer reads the scene through interfaces the other tests cover, and the nightly
# runs them for everything.
gpu_paths() {
while IFS= read -r -d '' path; do
case "$path" in
crates/render/*|crates/gpu/src/path/*|crates/ui-egui/src/render_workspace*|assets/render/*) gpu_paths=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.
group_has_render_label() {
for pr in $(git log --format=%s "$1" | grep -o '(#[0-9]*)$' | tr -d '(#)'); do
if gh api "repos/$REPO/issues/$pr" --jq '.labels[].name' | grep -qx render-tests; then
return 0
fi
done
return 1
}
if [ "$EVENT_NAME" = push ]; then
# The queue lands the very commit its merge_group run tested. If that run passed,
# testing the push again proves nothing new; otherwise (a merge that skipped the
@@ -191,12 +205,12 @@ jobs:
if [ "${tested:-0}" -gt 0 ]; then
echo "The merge queue already tested $PUSH_SHA: repository checks only."
elif tested=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=$GITHUB_REF_NAME&status=success&per_page=1" \
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=1" \
| grep -o '"head_sha": *"[0-9a-f]*"' | head -n 1 | grep -o '[0-9a-f]\{40\}') \
&& [ -n "$tested" ] && git merge-base --is-ancestor "$tested" "$PUSH_SHA" \
&& git diff --no-renames --name-only -z "$tested" "$PUSH_SHA" > "$RUNNER_TEMP/ci-paths" \
&& select_packages "$RUNNER_TEMP/ci-paths" && [ "$full" != true ] && [ -z "$packages" ]; then
# Nothing a build reads changed since the last push to this branch whose run passed (the
# Nothing a build reads changed since the last push to main whose run passed (the
# push before may never have run: a newer push cancels a waiting one).
echo "$tested..$PUSH_SHA changes no code: repository checks only."
else
@@ -213,19 +227,32 @@ jobs:
# the nightly runs it whatever changed. A group that can't be compared runs it.
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"
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
fi
echo "windows=$windows for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
echo "windows=$windows gpu_paths=$gpu_paths for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
elif [ "$EVENT_NAME" != pull_request ]; then
# The nightly run and Run workflow: everything, Windows included.
native=true
windows=true
gpu_paths=true
else
# Compare both old and new paths of renames; unknown paths run native checks.
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
while IFS= read -r -d '' path; do
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
# on any other change they would only re-check unchanged results; the merge queue,
# main and the nightly run every seed before anything lands.
case "$path" in
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
esac
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"
@@ -233,6 +260,10 @@ jobs:
# selected means no code changed.
select_packages "$RUNNER_TEMP/ci-paths"
if [ "$full" = true ] || [ -n "$packages" ]; then native=true; fi
# The goldens are solidcraft-engine tests: its tests must be in the archive.
if [ "$goldens" = true ] && [ "$full" != true ]; then
case " $packages " in *" solidcraft-engine "*) ;; *) packages="$packages solidcraft-engine" ;; esac
fi
fi
# The reference tests replay recipes through solidcraft-cli: needed when it, or anything
# it depends on, is selected.
@@ -247,12 +278,13 @@ jobs:
doctests=$native
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; doctests=false; fi
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
# Clippy, the tests and the doc tests.
# Clippy, the tests, the doc tests and the goldens.
quick=false
if [ "$EVENT_NAME" = pull_request ] && [ "$APPROVED" != true ]; then
quick=$native
native=false
oracle=false
goldens=false
fi
# A push that only merges main into the change (Merge branch 'main', Update branch) when
# the change itself already passed: nothing new to test here, and main tests the merged
@@ -265,19 +297,33 @@ jobs:
if [ -n "$carried" ]; then
echo "Only a merge of main into a change that passed in $carried: repository checks only."
native=false
goldens=false
fi
fi
# The GPU path tests (their own job) run with the other tests, when solidcraft-gpu is
# among the packages they test.
# The GPU path tests (their own job, about 56 runner-minutes) are for render changes only:
# - on a pull request, when it carries the render-tests label (opt-in for a big render
# change) or touches render code (gpu_paths); no other pull request runs them;
# - in the merge queue, when the group's diff touches render code or one of its pull
# requests carries render-tests;
# - in the nightly and by hand, always, so anything the above skipped is caught within a day;
# - not on a push to main: the queue tested that commit, or the nightly will.
gpu=false
if [ "$native" = true ]; then
case " $packages " in *" solidcraft-gpu "*) gpu=true ;; esac
if [ "$full" = true ]; then gpu=true; fi
case "$EVENT_NAME" in
pull_request)
gpu_paths "$RUNNER_TEMP/ci-paths"
gpu=$gpu_paths
if [ "$RENDER_LABEL" = true ]; then gpu=true; fi ;;
merge_group) gpu=$gpu_paths ;;
push) gpu=false ;;
*) gpu=true ;;
esac
fi
{
echo "native=$native"
echo "gpu=$gpu"
echo "kernel=$kernel"
echo "goldens=$goldens"
echo "windows=$windows"
echo "oracle=$oracle"
echo "doctests=$doctests"
@@ -301,7 +347,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: repo-checks
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Format
run: cargo fmt --all -- --check
- name: Asset attribution
@@ -348,10 +394,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: quick-compile
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Cargo check
run: cargo check --workspace --all-targets --locked
@@ -370,10 +413,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-clippy
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Clippy
env:
FULL: ${{ needs.changes.outputs.full }}
@@ -416,10 +456,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -490,10 +527,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -504,8 +538,13 @@ jobs:
- name: Run tests
env:
EVENT_NAME: ${{ github.event_name }}
GOLDENS: ${{ needs.changes.outputs.goldens }}
FULL: ${{ needs.changes.outputs.full }}
PACKAGES: ${{ needs.changes.outputs.packages }}
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
# queue and the nightly run every seed.
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
run: |
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml); `default()`
# below is that filter, so a pull request still runs only what it ran before.
@@ -519,23 +558,29 @@ jobs:
# The boolean property tests draw new random inputs every run; they run only in the
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
base='not (package(solidcraft-kernel) & binary(boolean_props))'
elif [ "$GOLDENS" = true ]; then
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
# also checks every seed but the slow ones, in the same run so they share its cores.
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
fi
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
rm -f "$junit"
# Pull requests test the affected packages (scripts/ci/affected.py).
scope=(--workspace)
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
status=0
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
# took about 36 of this job's 45 minutes.
# A failed test does not fail the step by itself: the tests known to fail today
# (tests/KNOWN_FAILING_TESTS) may fail, any other fails it, and so does a listed test
# that passes. nextest's exit code 100 is "tests failed"; any other is its own error
# (a build error, a bad filter) and fails the step at once.
status=0
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
cargo xtask known-failing --junit "$junit"
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=1
if [ "$status" != 0 ]; then
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
fi
exit 1
fi
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
@@ -609,10 +654,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-gpu
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -627,14 +669,8 @@ jobs:
# None of these is in the default filter (the heavy tests), so pull requests, the merge
# queue, main and nightly all run every one of them, as before. `count` deals the tests
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
# Graded against tests/KNOWN_FAILING_TESTS, as in Native Linux tests.
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
rm -f "$junit"
status=0
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4" || status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
cargo xtask known-failing --junit "$junit"
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4"
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
@@ -768,10 +804,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: doc-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -829,10 +862,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: oracle
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
@@ -871,10 +901,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-fxc
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
# One test at a time: WARP's shader JIT crashes now and then (a null read in
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
@@ -898,10 +925,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-startup
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Backend order and probing on Windows
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
- name: Build the app
@@ -1022,10 +1046,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: wasm
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo xtask wasm
riscv64-gates:
@@ -1047,10 +1068,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: riscv64
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh riscv64gc-unknown-linux-gnu
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cross-compilation dependencies
run: |
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
@@ -1066,13 +1084,11 @@ jobs:
EOF
sudo dpkg --add-architecture riscv64
sudo apt-get update
sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
sudo apt-get install -y gcc-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 libxi-dev:riscv64 libgl1-mesa-dev:riscv64
- name: Cargo check on RISC-V
env:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
# SMLib has no prebuilt library for RISC-V: its sources are compiled with this.
CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++
PKG_CONFIG_SYSROOT_DIR: /
PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
PKG_CONFIG_ALLOW_CROSS: 1
-3
View File
@@ -53,9 +53,6 @@ jobs:
with:
key: docker-web
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
- name: Install trunk
run: |
mkdir -p "$HOME/.local/bin"
-3
View File
@@ -29,9 +29,6 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- name: Build the CLI
run: cargo build --profile ci -p solidcraft-cli
- name: Import and check every entry
-109
View File
@@ -1,109 +0,0 @@
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
# runs every case in tests/kernel-cases/fillets/ on SMLib, through one release build of
# solidcraft-cli, and fails on a regression (a case that fails and
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
#
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
# crates/engine/src/cmd/features.rs select it whenever those files change. Pushes to main and
# develop that touch the same paths run it too and save the compiler cache (a Blacksmith sticky
# disk) that pull requests then mount read-only.
name: Fillet rig
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "crates/kernel/src/blend*.rs"
- "crates/kernel/src/fillet*.rs"
- "crates/kernel/src/*corner*.rs"
- "crates/kernel/src/continuity*.rs"
- "crates/smlib/**"
- "crates/doc/src/eval*.rs"
- "crates/engine/src/cmd/features.rs"
- "tests/kernel-cases/fillets/**"
# The rig itself: the grading, the runner and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "scripts/fetch-smlib.sh"
- ".github/workflows/fillet-rig.yml"
push:
branches: [main, develop]
# The same paths as pull_request.
paths:
- "crates/kernel/src/blend*.rs"
- "crates/kernel/src/fillet*.rs"
- "crates/kernel/src/*corner*.rs"
- "crates/kernel/src/continuity*.rs"
- "crates/smlib/**"
- "crates/doc/src/eval*.rs"
- "crates/engine/src/cmd/features.rs"
- "tests/kernel-cases/fillets/**"
# The rig itself: the grading, the runner and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "scripts/fetch-smlib.sh"
- ".github/workflows/fillet-rig.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: fillet-rig-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
fillets:
name: Fillet rig
# Draft pull requests run no CI, as in ci.yml.
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/runner/.cache/sccache
SCCACHE_CACHE_SIZE: 10G
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Size parallel compiles to the job's memory
run: scripts/ci/build-jobs.sh
- uses: dtolnay/rust-toolchain@stable
# Only main and develop commit the cache; pull requests and forks mount a clone and never
# write back.
- name: Mount the compiler cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-sccache-fillet-rig
path: /home/runner/.cache/sccache
commit: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: fillet-rig
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
- name: Build the CLI (release, SMLib)
run: cargo build --release --locked -p solidcraft-cli --features smlib
- name: Run the fillet cases against the known-failing list
run: >-
cargo run -q --locked -p xtask -- kernel-cases --area fillets
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
- name: Keep the results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fillet-rig-results
path: ${{ runner.temp }}/fillet-rig/results.json
if-no-files-found: ignore
-3
View File
@@ -48,9 +48,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: generated-files
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- name: THIRD-PARTY-LICENSES.txt
run: cargo xtask licences
- name: docs/features.md
-234
View File
@@ -1,234 +0,0 @@
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes; the ladder runs whenever any
# area does. One job then builds solidcraft-cli once (release, SMLib prebuilt), checks that every
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
#
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
# request, so the list only shrinks).
# - The ladder fails on any case whose status or counts differ from
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
#
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
# other run mounts read-only.
name: Kernel rig
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "crates/kernel/**"
- "crates/geom/**"
- "crates/smlib/**"
- "crates/step/**"
- "crates/io/src/**"
- "crates/doc/src/**"
- "crates/engine/src/**"
- "tests/kernel-cases/**"
# The rig itself: the grading, the runner, the selection and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "xtask/src/kernel_ladder.rs"
- "scripts/fetch-smlib.sh"
- "scripts/ci/kernel_rig_plan.py"
- "scripts/ci/test_kernel_rig_plan.py"
- ".github/workflows/kernel-rig.yml"
push:
branches: [main, develop]
# The same paths as pull_request.
paths:
- "crates/kernel/**"
- "crates/geom/**"
- "crates/smlib/**"
- "crates/step/**"
- "crates/io/src/**"
- "crates/doc/src/**"
- "crates/engine/src/**"
- "tests/kernel-cases/**"
# The rig itself: the grading, the runner, the selection and this workflow.
- "apps/solidcraft-cli/src/kernel_cases.rs"
- "xtask/src/kernel_cases.rs"
- "xtask/src/kernel_ladder.rs"
- "scripts/fetch-smlib.sh"
- "scripts/ci/kernel_rig_plan.py"
- "scripts/ci/test_kernel_rig_plan.py"
- ".github/workflows/kernel-rig.yml"
merge_group:
workflow_call:
workflow_dispatch:
permissions:
contents: read
concurrency:
# A new push to a pull request replaces its old run.
group: kernel-rig-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
plan:
name: Select the rig areas
# Draft pull requests run no CI, as in ci.yml.
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
shards: ${{ steps.plan.outputs.shards }}
# Whether the build job, and with it the coverage check, runs.
build: ${{ steps.plan.outputs.build }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# A pull request's diff needs its base's history.
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
- name: Selection self-test
run: python3 -I scripts/ci/test_kernel_rig_plan.py
- id: plan
name: Map the changed files to areas
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
# gate and a run by hand run every area.
if [ "$EVENT_NAME" = pull_request ]; then
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --github-output "$GITHUB_OUTPUT"
else
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
fi
build:
name: Build the CLI
needs: plan
if: needs.plan.outputs.build == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/runner/.cache/sccache
SCCACHE_CACHE_SIZE: 10G
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Size parallel compiles to the job's memory
run: scripts/ci/build-jobs.sh
- uses: dtolnay/rust-toolchain@stable
# Only pushes to main and develop commit the cache; pull requests, merge groups and forks
# mount a clone and never write back.
- name: Mount the compiler cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-sccache-kernel-rig
path: /home/runner/.cache/sccache
commit: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: kernel-rig
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
- name: Build the CLI (release, SMLib) and the rig's runner
run: |
cargo build --release --locked -p solidcraft-cli --features smlib
cargo build --locked -p xtask
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
# job of this workflow.
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
- name: Every command has a case
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
- name: Pack the binaries
if: needs.plan.outputs.shards != '[]'
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: needs.plan.outputs.shards != '[]'
with:
name: kernel-rig-bin
path: ${{ runner.temp }}/kernel-rig-bin.tar
retention-days: 1
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
shard:
name: Kernel rig (${{ matrix.shard }})
needs: [plan, build]
if: needs.plan.outputs.shards != '[]'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
# needs splitting.
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: kernel-rig-bin
path: ${{ runner.temp }}
- name: Unpack the binaries
run: mkdir -p target && tar -xf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target
- name: Run the cases against the known-failing list (the ladder against its golden)
env:
SHARD: ${{ matrix.shard }}
run: |
if [ "$SHARD" = ladder ]; then
set -- --ladder --gate
else
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
fi
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
- name: Keep the results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: kernel-rig-results-${{ matrix.shard }}
path: ${{ runner.temp }}/kernel-rig/results.json
if-no-files-found: ignore
# One check for the whole gate, for branch protection and the merge queue.
kernel-rig:
name: kernel-rig
needs: [plan, build, shard]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
# A skipped build or shard counts as passed only when the plan selected none.
- name: Every selected job passed
env:
PLAN: ${{ needs.plan.result }}
WANT_BUILD: ${{ needs.plan.outputs.build }}
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
BUILD: ${{ needs.build.result }}
SHARDS: ${{ needs.shard.result }}
run: |
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
test "$PLAN" = success
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
+9 -7
View File
@@ -55,13 +55,15 @@ jobs:
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
xmllint --noout assets/platforms/windows/solidcraft.wxs assets/platforms/windows/solidcraft.en-us.wxl assets/platforms/macos/*.plist* assets/platforms/linux/*.xml*
desktop-file-validate assets/platforms/linux/ai.storyteller.solidcraft.desktop
# The window's X11 class is its app id; the old id stays only as the rename shims.
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' assets/platforms/linux/ai.storyteller.solidcraft.desktop
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in
desktop-file-validate assets/platforms/linux/io.tangencylabs.solidcraft.desktop
# The window's X11 class is its app id; the old ids stay only as the rename shims.
grep -qx 'StartupWMClass=io.tangencylabs.solidcraft' assets/platforms/linux/io.tangencylabs.solidcraft.desktop
grep -q '<id>ai.storyteller.solidcraft</id>' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in
grep -qx 'X-Flatpak-RenamedFrom=ai.storyteller.solidcraft.desktop;ai.storyteller.solvecraft.desktop;' assets/platforms/linux/io.tangencylabs.solidcraft.desktop
grep -qF -e '--end-of-life-rebase=' packaging/flatpak-site.sh
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solidcraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solidcraft' and m['command'] == 'solidcraft'"
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/io.tangencylabs.solidcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/io.tangencylabs.solidcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/io.tangencylabs.solidcraft.bundle.yml')); assert m['id'] == 'io.tangencylabs.solidcraft' and m['command'] == 'solidcraft'"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
python3 -m py_compile packaging/linux/snap-smoke.py
-3
View File
@@ -41,9 +41,6 @@ jobs:
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
- name: Install mdbook and trunk
run: |
mkdir -p "$HOME/.local/bin"
-74
View File
@@ -1,74 +0,0 @@
# The release gate: everything, in full and in parallel, on one branch's head, before that branch
# replaces main as a release (develop, rebased onto main). Run it by hand:
# gh workflow run release-gate.yml --ref develop
# It calls the workflows that own each check instead of copying their jobs:
# - ci.yml as Run workflow with cross-compilation: every native suite in full (not scoped), the
# reference tests, the doc tests, the GPU path tests, the action matrix (all 16 shards) and its
# determinism check, the property tests, the repository checks (web-offline among them),
# WebAssembly and RISC-V, Windows D3D12 FXC and Windows app startup;
# - kernel-rig.yml: every rig area against its KNOWN_FAILING and the ladder against its golden;
# - release.yml as a dry run (draft false, packages_only): the Linux, Flatpak, Snap, FreeBSD,
# macOS, Windows and web packages with their smoke tests and package checks, unsigned (no
# secrets are passed, so no signing secret or SignPath quota is used) and without the release
# job (checksums, update signing, publishing).
# The gate passes only when all three pass. Before main is replaced, tag the old main
# (main-before-develop-<date>) and push the tag.
name: Release gate
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-gate-${{ github.ref }}
cancel-in-progress: false
jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
permissions:
contents: read
# Select checks reads this commit's other runs.
actions: read
with:
cross_compile: true
kernel-rig:
name: Kernel rig
uses: ./.github/workflows/kernel-rig.yml
permissions:
contents: read
packages:
name: Packages
uses: ./.github/workflows/release.yml
# No secrets: the signing steps skip without them. GitHub checks a called workflow's job
# permissions before the run, skipped jobs included, so the release job's contents: write must
# be granted here; packages_only skips that job, and no step that runs writes to the
# repository. actions: read is the Windows job's.
permissions:
contents: write
actions: read
with:
draft: false
packages_only: true
release-gate:
name: release-gate
needs: [ci, kernel-rig, packages]
if: always()
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Every part passed
env:
CI: ${{ needs.ci.result }}
KERNEL_RIG: ${{ needs.kernel-rig.result }}
PACKAGES: ${{ needs.packages.result }}
run: |
echo "ci=$CI kernel-rig=$KERNEL_RIG packages=$PACKAGES"
test "$CI" = success
test "$KERNEL_RIG" = success
test "$PACKAGES" = success
+4 -41
View File
@@ -20,8 +20,6 @@
# releases/latest/download/<name> links in the README keep working across releases.
#
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
# The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and
# without the release job (packages_only).
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
name: Release
@@ -42,25 +40,6 @@ on:
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
type: string
default: ""
# The release gate (release-gate.yml): a dry run (draft false) of every package and its checks.
# The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does.
# The gate passes no secrets, so nothing is signed (the signing steps skip without their
# secrets, as on a fork), and packages_only skips the release job (checksums, update signing,
# publishing).
workflow_call:
inputs:
packages_only:
description: "Build and check every package only: no release job"
type: boolean
default: false
draft:
description: "Create/update a draft (release branch only; off: build artifacts only)"
type: boolean
default: false
signpath_signing_policy:
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
type: string
default: ""
permissions:
contents: read
@@ -125,9 +104,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-icons
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- run: cargo build --release --locked -p solidcraft-cli
- run: |
. packaging/env.sh
@@ -167,9 +143,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-linux-${{ matrix.arch }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh
- name: Build and package (AppImage, deb, rpm, tar.gz)
run: packaging/linux/package.sh
- name: Check the packages
@@ -324,7 +297,7 @@ jobs:
export CARGO_TARGET_DIR=/var/tmp/solidcraft-target CARGO_BUILD_JOBS=4
bash packaging/freebsd/package.sh
tar -tzf dist/release/solidcraft-*-freebsd-x86_64.tar.gz \
| grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/ai\.storyteller\.solvecraft\.desktop$|/share/doc/solidcraft/THIRD-PARTY-LICENSES\.txt$'
| grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/io\.tangencylabs\.solidcraft\.desktop$|/share/doc/solidcraft/THIRD-PARTY-LICENSES\.txt$'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-freebsd-x86_64
@@ -337,7 +310,7 @@ jobs:
name: macOS build ${{ matrix.arch }}
needs: version
if: needs.version.outputs.build == 'true'
runs-on: macos-15
runs-on: macos-14
strategy:
fail-fast: false
matrix:
@@ -355,9 +328,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-macos-${{ matrix.arch }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh ${{ matrix.target }}
- name: Build
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
# Not named release-*: the release job collects every release-* artifact as an asset.
@@ -374,7 +344,7 @@ jobs:
name: macOS universal
needs: [version, macos-build]
if: needs.version.outputs.build == 'true'
runs-on: macos-15
runs-on: macos-14
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
@@ -456,9 +426,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-windows-${{ matrix.arch }}
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh ${{ matrix.target }}
- name: Install WiX
shell: pwsh
run: |
@@ -590,9 +557,6 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-web
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
shell: bash
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
- name: Install trunk
run: |
curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
@@ -608,8 +572,7 @@ jobs:
release:
name: GitHub Release or dry-run artifacts
needs: [version, linux, flatpak, snap, freebsd, macos, windows, web]
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
if: needs.version.outputs.build == 'true' && !inputs.packages_only
if: needs.version.outputs.build == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
-1
View File
@@ -45,4 +45,3 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
+83 -27
View File
@@ -101,22 +101,6 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/kernel-cases/import/1721-closed-bspline-tube-face/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#1721 | CC0-1.0 |
| tests/kernel-cases/import/2043-outer-bound-opposing-surface-sense/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#2043 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/258-cone-point-vertex/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-cone-to-point/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-tiny-pin/input.step | step-conformance contributors | copy of tests/step-conformance/tiny_pin.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/3098-bspline-edges-over-tiny-knot-span/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3098 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3208-extruded-ellipse-face-over-parameter-seam/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3208 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3352 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3730-void-oriented-shell-false/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3730 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3880-cylinder-seam-edge/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_seam.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3880-nurbs-hemisphere-dome/input.step | step-conformance contributors | copy of tests/step-conformance/dome_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3963-closed-spline-edge-vertex-past-start/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3963 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4005-cylinder-written-as-near-zero-cone/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4005 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4006-plane-written-as-cone-of-nearly-right-angle/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4006 | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/*.json.gz, tests/ssi-corpus/LICENSE-MIT, tests/ssi-corpus/LICENSE-APACHE | ssi-corpus contributors (jyviko) | 119 surface/surface intersection cases with exact ground truth from https://github.com/jyviko/ssi-corpus (cases/data, b04c8a6), gzip-compressed, unchanged; SSI scorer fixtures (tests/ssi-corpus/README.md) | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/y23_*.json.gz | Yang, Jia and Yan (paper authors: published surfaces, figures and topology); ssi-corpus contributors (reconstructions and truth) | 25 cases built from Yang, Jia & Yan 2023, "Topology Guaranteed B-Spline Surface/Surface Intersection", ACM Transactions on Graphics 42(6), Article 211; each cites the paper in its `source` field | CC-BY 4.0 (paper); MIT OR Apache-2.0 (case files) |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -144,14 +128,14 @@ Editable drawing code, native projects and shipped exports are collected under `
| 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 |
| assets/icons/app/linux/exports/hicolor/128x128/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/16x16/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/24x24/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/128x128/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/16x16/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/24x24/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/32x32/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/48x48/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -187,9 +171,9 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/zh-TW.json | SolidCraft contributors | Original Traditional Chinese (Taiwan) translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/io.tangencylabs.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/io.tangencylabs.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/io.tangencylabs.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
@@ -308,6 +292,53 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
### Render workspace icon drafts
Original SolidCraft drawings, Open for owner review; no runtime assignments.
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icons/ui/design/svg/model/render_all_shots.svg | SolidCraft contributors | original Camera icon master (64 grid): stacked camera bodies with a lower-right play badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_appearance.svg | SolidCraft contributors | original Looks icon master (64 grid): shaded look sphere on the top corner of an accent-shaded cube | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_aspect_frame.svg | SolidCraft contributors | original Camera icon master (64 grid): wide rounded frame with two opposite corner brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_backdrop.svg | SolidCraft contributors | original Studio icon master (64 grid): grey cube in front of a tall upright accent panel | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_cancel.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round stop badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_check.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round check badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_compare.svg | SolidCraft contributors | original Output icon master (64 grid): two grey pictures split by a vertical slider with opposed arrows | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_copy_look.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere in a square frame offset from a second frame outline | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_execute.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a lower-right round play badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_export_image.svg | SolidCraft contributors | original Output icon master (64 grid): picture with a large outward diagonal arrow at the upper right | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_exposure.svg | SolidCraft contributors | original Camera icon master (64 grid): split light and accent disc with plus and minus marks | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_fit_product.svg | SolidCraft contributors | original Camera icon master (64 grid): grey cube inside four corner brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_frame_guides.svg | SolidCraft contributors | original Camera icon master (64 grid): rounded frame with a rule-of-thirds grid and four accent nodes | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_from_material.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere struck through above a plain grey slab | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_ground.svg | SolidCraft contributors | original Studio icon master (64 grid): grey cube on a slab with an accent contact patch | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_lens.svg | SolidCraft contributors | original Camera icon master (64 grid): concentric lens rings around an accent centre | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_library.svg | SolidCraft contributors | original Studio icon master (64 grid): row of three look spheres, middle one accent, on a shelf bar | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_area.svg | SolidCraft contributors | original Lights icon master (64 grid): slanted flat panel above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_by_highlight.svg | SolidCraft contributors | original Lights icon master (64 grid): shaded sphere with a ringed highlight target and dotted stem arriving from above | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_point.svg | SolidCraft contributors | original Lights icon master (64 grid): round source disc with centre dot above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_light_spot.svg | SolidCraft contributors | original Lights icon master (64 grid): cone source with curved rim above a dotted accent stem | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_lighting_presets.svg | SolidCraft contributors | original Studio icon master (64 grid): two stacked cards, the front one holding a light bulb | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_output_size.svg | SolidCraft contributors | original Output icon master (64 grid): picture frame with a diagonal double-headed dimension arrow | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_paint.svg | SolidCraft contributors | original Looks icon master (64 grid): flat brush with an accent tip, light ferrule and slate handle | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_pick_up.svg | SolidCraft contributors | original Looks icon master (64 grid): eyedropper with a grey bulb, slate crossbar and accent sampling tip | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_quality.svg | SolidCraft contributors | original Output icon master (64 grid): three rising bars, the tallest in accent | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_replace_all.svg | SolidCraft contributors | original Looks icon master (64 grid): grey sample sphere with an arrow to an accent sphere beside a second one | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_results.svg | SolidCraft contributors | original Output icon master (64 grid): stack of two pictures, the front one showing a landscape | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_same_look.svg | SolidCraft contributors | original Looks icon master (64 grid): look sphere and three cubes, the two matching cubes ringed | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_shot.svg | SolidCraft contributors | original Camera icon master (64 grid): camera body with a lower-right plus badge | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_shots_strip.svg | SolidCraft contributors | original Camera icon master (64 grid): strip of three small frames, the middle one in accent | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_solo_light.svg | SolidCraft contributors | original Lights icon master (64 grid): point source and dotted stem inside four corner focus brackets | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_sun.svg | SolidCraft contributors | original Lights icon master (64 grid): source disc with eight short radial rays | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_transparent_background.svg | SolidCraft contributors | original Output icon master (64 grid): grey cube on a checker tile | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_update_shot.svg | SolidCraft contributors | original Camera icon master (64 grid): camera body with a lower-right refresh badge | MIT OR Apache-2.0 |
### UI icon masters (continued)
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/configuration_table.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/conic.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -397,6 +428,21 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_environment.svg | SolidCraft contributors | original Studio icon master (64 grid): dome with a grey cube inside | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_shaded.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_wireframe.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_hidden_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_translucent.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_faces_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_bodies.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_features.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_parts.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_sketch.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/display.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/grid.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ground_shadow.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ambient_occlusion.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -546,6 +592,11 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/cone.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/locales/ar.json | Abdullah (A-AL-ANAZI) | Original Arabic translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/he.json | SolidCraft contributors | Original Hebrew translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/styles/ui/themes/cga.json | SolidCraft contributors | original SolidCraft theme file using the 16 colours of the IBM Color Graphics Adapter (black, cyan, magenta, and white of its high-intensity palette 1 for the interface) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit-dark.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-customisation.json` | SolidCraft contributors | Original navigation bar editor translations | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-slots.json` | SolidCraft contributors | Original navigation slot translations | MIT OR Apache-2.0 |
## Code adapted from sibling projects
@@ -605,6 +656,11 @@ copied into SolidCraft.
| Directory | Source | Licence | Changes |
|---|---|---|---|
| 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) |
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
## Vendored renderer compatibility fix
+13 -8
View File
@@ -136,6 +136,7 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
command without them, and will fail it once required), engine tests that measure the result (volume, area,
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
A command on the toolbar also ships with its help tooltip (step 6 of Adding a command).
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
`navigation::handle` (`crates/ui-egui/src/navigation.rs`) for its camera input, with
@@ -158,11 +159,9 @@ the most complete description of the house style.
| UI scenarios | `crates/ui-egui/tests/scenarios/*.json` | part of `cargo test` |
| Reference tests | `cargo xtask oracle` | `tests/oracle/` (see below) |
The reference for what the kernel builds is the kernel-cases ladder (`tests/kernel-cases/ladder/`,
`cargo xtask kernel-cases --ladder`, [docs/kernel-ladder.md](docs/kernel-ladder.md)).
The naming corpus checks persistent names: every name a corpus design stores still finds a face or
an edge, and two builds name its faces and edges alike
(`cargo test --profile ci -p solidcraft-cli --test naming_corpus`).
A geometry change that changes the robustness goldens re-records them in the same pull request:
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
black-box suites miss.
@@ -221,7 +220,8 @@ Pull requests skip a few heavy tests (one to twelve minutes each), which run in
Check that validation after merging. They are listed in
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
@@ -241,8 +241,7 @@ the surface itself). Expected values are either worked out in closed form
(`tools/oracle-analytic/`) or computed by building the same recipe in FreeCAD, run as a separate
program (`tools/freecad-oracle/`); each file says which. The generated
[reference test report](docs/oracle.md) has the current case count and results. The reference-test
CI job runs `cargo xtask oracle` during full validation, and on pull requests that reach
`solidcraft-cli`. A case that
CI job runs `cargo xtask oracle` during full validation; pull-request runs skip it. A case that
fails because of a known bug names its issue in `expected.json` (`"known_failure": "#123"`);
never change an expected value to match SolidCraft's output.
@@ -285,6 +284,12 @@ and a fresh CI run:
- **Commands without schemas** (`crates/engine/src/schema/unconverted.txt`): sorted, one id per
line, and it only shrinks. Converting a command removes its line; a new command is never added
to it.
- **Toolbar commands without help** (`BACKLOG` in `crates/ui-egui/src/command_help.rs`): sorted,
one id per line, and it only shrinks. Writing a command's help removes its line; a new command
is never added to it.
- **`ui.prefs` settings** (`docs/control-protocol.md`): one line per setting, sorted by name, in
the list under the protocol table; add a new setting on its own line rather than editing a shared
row.
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
checks it).
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
Generated
+326 -55
View File
@@ -188,6 +188,15 @@ dependencies = [
"libc",
]
[[package]]
name = "approx"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
dependencies = [
"num-traits",
]
[[package]]
name = "arboard"
version = "3.6.1"
@@ -208,6 +217,12 @@ dependencies = [
"x11rb",
]
[[package]]
name = "array-macro"
version = "2.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -520,6 +535,15 @@ dependencies = [
"piper",
]
[[package]]
name = "branches"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0"
dependencies = [
"rustc_version",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -767,6 +791,17 @@ dependencies = [
"libc",
]
[[package]]
name = "cgmath"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
dependencies = [
"approx",
"num-traits",
"serde",
]
[[package]]
name = "chacha20"
version = "0.10.2"
@@ -789,6 +824,19 @@ dependencies = [
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -859,6 +907,12 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1036,6 +1090,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "derive_more"
version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.119",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -1182,7 +1249,7 @@ dependencies = [
"bitflags 2.13.2",
"emath",
"epaint",
"itertools",
"itertools 0.15.0",
"log",
"nohash-hasher",
"profiling",
@@ -1600,8 +1667,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -1914,6 +1983,30 @@ dependencies = [
"typenum",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -2073,6 +2166,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.15.0"
@@ -2167,6 +2269,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "katexit"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
@@ -2317,6 +2430,15 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "matext4cgmath"
version = "0.1.0"
dependencies = [
"cgmath",
"katexit",
"num-complex",
]
[[package]]
name = "memchr"
version = "2.8.3"
@@ -3192,6 +3314,30 @@ dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -3364,6 +3510,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rclite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2"
dependencies = [
"branches",
]
[[package]]
name = "read-fonts"
version = "0.41.0"
@@ -4053,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4087,24 +4242,23 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.24.0"
version = "0.25.1"
[[package]]
name = "solidcraft-cli"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"log",
"serde_json",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-kernel",
"solidcraft-mcp",
]
[[package]]
name = "solidcraft-derive"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4113,7 +4267,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"log",
"serde",
@@ -4122,7 +4276,6 @@ dependencies = [
"solidcraft-derive",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
@@ -4131,7 +4284,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"log",
@@ -4143,7 +4296,6 @@ dependencies = [
"solidcraft-geom",
"solidcraft-io",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
@@ -4153,7 +4305,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
"serde_json",
@@ -4162,7 +4314,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"log",
@@ -4182,7 +4334,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4200,14 +4352,13 @@ dependencies = [
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-profile",
"thiserror 2.0.21",
"zip",
]
[[package]]
name = "solidcraft-kernel"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"log",
"proptest",
@@ -4215,34 +4366,28 @@ dependencies = [
"serde",
"serde_json",
"solidcraft-geom",
"solidcraft-profile",
"solidcraft-smlib",
"solidcraft-step",
"spade",
"thiserror 2.0.21",
"truck-meshalgo",
"truck-modeling",
"truck-shapeops",
"truck-stepio",
"truck-topology",
"web-time",
]
[[package]]
name = "solidcraft-mcp"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde_json",
"solidcraft-engine",
"solidcraft-gpu",
]
[[package]]
name = "solidcraft-profile"
version = "0.24.0"
dependencies = [
"serde_json",
"web-time",
]
[[package]]
name = "solidcraft-render"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"moxcms",
@@ -4256,7 +4401,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
"serde_json",
@@ -4265,37 +4410,16 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "solidcraft-smlib"
version = "0.24.0"
dependencies = [
"cc",
"flate2",
"log",
"serde_json",
"solidcraft-geom",
"solidcraft-profile",
"web-time",
]
[[package]]
name = "solidcraft-space-mouse"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-step"
version = "0.24.0"
dependencies = [
"solidcraft-geom",
"solidcraft-profile",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"arboard",
"egui",
@@ -4325,7 +4449,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"eframe",
"egui",
@@ -4386,6 +4510,16 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.119"
@@ -4623,6 +4757,144 @@ dependencies = [
"once_cell",
]
[[package]]
name = "truck-base"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c279de9e92e5dc20a188deb0bb9a4bd421a6a185e57e03e19025e84a36c5a05"
dependencies = [
"cgmath",
"matext4cgmath",
"rustc-hash 2.1.3",
"serde",
]
[[package]]
name = "truck-derivers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421ff1c5a303aed64e295d9b30e8424ee7a2fa251617a59ae4acfd078496009f"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "truck-geometry"
version = "0.5.0"
dependencies = [
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "truck-geotrait"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a31fe98d5a0589ad20576260e214af23dea21a8d125da81ad3bbcb666f411f"
dependencies = [
"getrandom 0.2.17",
"rand 0.8.8",
"thiserror 1.0.69",
"truck-base",
"truck-derivers",
]
[[package]]
name = "truck-meshalgo"
version = "0.4.0"
dependencies = [
"array-macro",
"derive_more",
"itertools 0.13.0",
"rayon",
"rustc-hash 2.1.3",
"spade",
"truck-base",
"truck-geometry",
"truck-modeling",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-modeling"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8082c2dfac8c2732a014d81807fb5d24dbac0d72ee4de2a705b7fbc05fa05b18"
dependencies = [
"derive_more",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geometry",
"truck-geotrait",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-polymesh"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f22def310af4a89c37beb4839bbf702b302d2c112ea29553759f0e16622fbf"
dependencies = [
"array-macro",
"bytemuck",
"itertools 0.13.0",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "truck-shapeops"
version = "0.4.0"
dependencies = [
"derive_more",
"itertools 0.13.0",
"rustc-hash 2.1.3",
"truck-base",
"truck-geometry",
"truck-geotrait",
"truck-meshalgo",
"truck-topology",
]
[[package]]
name = "truck-stepio"
version = "0.3.0"
dependencies = [
"chrono",
"truck-geometry",
"truck-modeling",
"truck-polymesh",
"truck-topology",
]
[[package]]
name = "truck-topology"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf62a1610405c5e39ab5eb6948f326fc1641689734a6b8207a80792133fe0e78"
dependencies = [
"parking_lot",
"rayon",
"rclite",
"rustc-hash 2.1.3",
"serde",
"thiserror 1.0.69",
"truck-base",
"truck-geotrait",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -5722,10 +5994,9 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"proc-macro2",
"quick-xml",
"serde",
"serde_json",
"sha2 0.10.9",
+19 -8
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.24.0"
version = "0.25.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -16,11 +16,8 @@ repository = "https://github.com/bherbruck/solidcraft"
robust = "1.2"
# internal
solidcraft-geom = { path = "crates/geom" }
solidcraft-profile = { path = "crates/profile" }
solidcraft-sketch = { path = "crates/sketch" }
solidcraft-kernel = { path = "crates/kernel" }
solidcraft-smlib = { path = "crates/smlib" }
solidcraft-step = { path = "crates/step" }
solidcraft-render = { path = "crates/render" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
@@ -48,7 +45,14 @@ sha2 = "0.10"
moxcms = { version = "0.8.1", default-features = false, features = ["extended_range"] }
semver = "1"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0).
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
truck-modeling = "0.6"
truck-shapeops = { version = "0.4", default-features = false }
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
truck-stepio = { version = "0.3", default-features = false }
truck-topology = "0.6"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
# tree through truck-meshalgo.
spade = { version = "2.15", default-features = false, features = ["std"] }
rayon = "1.12"
@@ -121,7 +125,9 @@ opt-level = 2
opt-level = 1
# Tests and lints (`cargo xtask ci`, GitHub CI): quick to build, quick enough to run. Light
# optimisation, line tables only (file:line in backtraces), no LTO, no debug assertions.
# optimisation, line tables only (file:line in backtraces), no LTO. Debug assertions are off because
# truck's debug-only checks reject valid geometry (seam edges, closed edges); that was the only
# reason the tests used to build with --release, which cost most of CI's time.
[profile.ci]
inherits = "dev"
opt-level = 1
@@ -144,9 +150,14 @@ codegen-units = 16
# binary). See docs/crash-reports.md.
debug = "line-tables-only"
# Vendored crates with SolidCraft fixes (see vendor/README.md and ATTRIBUTION.md).
# truck-shapeops with SolidCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md).
[patch.crates-io]
truck-geometry = { path = "vendor/truck-geometry" }
truck-shapeops = { path = "vendor/truck-shapeops" }
truck-stepio = { path = "vendor/truck-stepio" }
truck-meshalgo = { path = "vendor/truck-meshalgo" }
matext4cgmath = { path = "vendor/matext4cgmath" }
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
egui-wgpu = { path = "vendor/egui-wgpu" }
+44
View File
@@ -0,0 +1,44 @@
# Disclaimer
This disclaimer covers the software, models, data, documentation and every other file in this
repository and in the releases built from it (together, "the files").
## No warranty
The files are provided "as is", without warranty of any kind, express or implied, including
warranties of accuracy, merchantability, fitness for a particular purpose and non-infringement.
Results produced with the files, such as geometry, meshes, analyses and simulations, can be
wrong. A qualified person must check them before anyone relies on them for a design,
manufacturing or safety decision.
## No liability
To the fullest extent permitted by law, Tangency Labs and the contributors are not liable for any
claim, damage, loss or other liability arising from the files or from their use, modification or
distribution, whether in contract, tort or otherwise.
## No indemnity
Neither Tangency Labs nor any contributor indemnifies, defends or holds harmless anyone who uses,
modifies or distributes the files, including against claims that the files infringe someone
else's rights.
## Harmful or unlawful use
Tangency Labs and the contributors do not authorise, endorse or accept any responsibility for use
of the files to harm people, animals, property or the environment, or for any unlawful use.
Whoever uses the files is solely responsible for how they use them, for what they make with them
and for complying with the laws that apply to them. Tangency Labs and the contributors must not be
named or held out as responsible for any such use.
## No endorsement
Using, modifying or distributing the files does not mean that Tangency Labs or any contributor
endorses you, your product or your results. Do not use the name of Tangency Labs or of a
contributor to suggest such an endorsement without written permission.
## Relation to the licence
This disclaimer adds to the warranty and liability terms of the licence the files are distributed
under. It does not restrict any right that licence grants; where the two differ, the licence
governs.
+10 -3
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png" width="96" height="96" alt="SolidCraft" />
<img src="assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" width="96" height="96" alt="SolidCraft" />
</p>
<h1 align="center">SolidCraft</h1>
@@ -74,7 +74,7 @@ other is a tracked bug.
- **Parameters and configurations** with unit-aware expressions.
- **Assemblies** with joints, motion studies, contact sets, interference and mass properties.
- **Sheet metal** with flanges, hems, fold/unfold and flat patterns, and plastic enclosure features.
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.solidcraft` designs.
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.sc` designs (`.solidcraft` files from earlier releases still open).
- **Desktop app, browser app, headless CLI and MCP server**, all running the same commands.
The full list is in [docs/capabilities.md](docs/capabilities.md) and [docs/features.md](docs/features.md);
@@ -121,11 +121,18 @@ the clean-room and licence rules, and where help is wanted most. Open work is in
SolidCraft's continuous integration runs on [Blacksmith](https://www.blacksmith.sh), which
sponsors its CI minutes.
## Disclaimer
The files in this repository are provided as is, without warranty, liability or indemnity, and
Tangency Labs and the contributors accept no responsibility for any harmful or unlawful use of
them: read [DISCLAIMER.md](DISCLAIMER.md).
## Licence
MIT OR Apache-2.0, at your option. Developed under the
[clean-room contribution rules](AGENTS.md); third-party code and assets retain their
documented licences and attribution.
documented licences and attribution. The licence covers the code, not the name and logo: see
[TRADEMARKS.md](TRADEMARKS.md) for how to name a fork or a product built on SolidCraft.
SolidCraft is independent. Behaviour is designed from public documentation, general CAD
conventions and user feedback. Reference tests use analytic values and open-source tools.
+36
View File
@@ -0,0 +1,36 @@
# SolidCraft name and logo
The code is yours to use under MIT OR Apache-2.0 (see [NOTICE](NOTICE)). The name is not part of
that licence: "SolidCraft" and the SolidCraft logo identify this project and the builds its
maintainers publish, and Tangency Labs, which maintains SolidCraft, uses them as its trademarks.
Neither licence grants rights to them.
This page says how you can use the name. It is meant to be easy to follow: most uses are fine.
## Fine without asking
- Saying what your work is built on: "based on SolidCraft", "a fork of SolidCraft", "works with
SolidCraft", "SolidCraft plugin for …".
- Writing, teaching, reviewing or making videos about SolidCraft, including in titles.
- Redistributing unmodified official builds under the SolidCraft name, for example in a Linux
distribution or package manager.
- Using the logo to link to the project or to show that something supports SolidCraft.
- User groups, meetups and community spaces about SolidCraft, as long as they don't look official.
## Use your own name
- A fork, a modified build or a product built on SolidCraft needs its own name and logo. "SolidCraft"
may appear in its description ("based on SolidCraft"), not as its name or the main part of it.
- Don't use names or logos that could be mistaken for SolidCraft, such as "SolidCraft Pro",
"SolidKraft" or a lightly changed version of the logo.
- Don't imply that SolidCraft's maintainers made, endorse or support your product or service unless
they do.
- Don't use the name in domain names, app store listings or social media accounts in a way that
looks official.
## Questions
If you're not sure whether a use is fine, open an issue or ask on [Discord](https://discord.gg/FtU4NQNVDH).
For anything this page doesn't allow, ask first; we're generally happy to say yes.
Other companies' trademarks named in this repository are covered in [NOTICE](NOTICE).
+1 -9
View File
@@ -20,15 +20,9 @@ name = "solvecraft-cli"
path = "src/alias.rs"
[features]
default = ["raytrace-cpu", "smlib", "profile"]
default = ["raytrace-cpu"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
# only kernel, so without it the command line models nothing.
smlib = ["solidcraft-engine/smlib"]
# The profiler, compiled in so any build can say where its time goes (`--profile`,
# SOLIDCRAFT_PROFILE=1); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
[dependencies]
solidcraft-engine = { workspace = true }
@@ -40,5 +34,3 @@ serde_json = { workspace = true }
[dev-dependencies]
# Snapshot tests decode the pictures to compare them with a tolerance.
image = { workspace = true }
# Tests measure meshes at their own tolerances.
solidcraft-kernel = { workspace = true }
@@ -68,7 +68,7 @@ fn run() -> Result<(), String> {
let generation_start = Instant::now();
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
let generation_seconds = generation_start.elapsed().as_secs_f64();
let design_path = output.with_extension("solidcraft");
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())?;
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
let bodies: Vec<Value> = scene
+8 -5
View File
@@ -8,7 +8,7 @@
//! no fixture.
//!
//! ```text
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.solidcraft]
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.sc]
//! ```
//! prints one JSON line per hundred commands (mean and max milliseconds, bodies so far), then the
//! build total, then the phases `bench-open` prints for the saved design.
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
run.push(t.elapsed().as_secs_f64() * 1000.0);
let t = std::time::Instant::now();
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
s.world_state().warm_meshes(|| true);
shown.push(t.elapsed().as_secs_f64() * 1000.0);
if run.len() == 100 || i + 1 == commands.len() {
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
@@ -198,14 +198,17 @@ pub fn run(args: &[String]) -> Result<(), String> {
shown.clear();
}
}
let world = s.world_state().map_err(|e| e.to_string())?;
let world = s.world_state();
println!(
"{}",
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
);
let path = match flag("--save") {
Some(p) => p.clone(),
None => std::env::temp_dir().join(format!("bench-agent-{}.solidcraft", std::process::id())).to_string_lossy().into_owned(),
None => std::env::temp_dir()
.join(format!("bench-agent-{}.{}", std::process::id(), solidcraft_engine::io::DESIGN_EXT))
.to_string_lossy()
.into_owned(),
};
s.execute("file.save", &json!({"path": path})).map_err(|e| e.to_string())?;
let r = crate::cmd_bench_open(std::slice::from_ref(&path));
@@ -233,7 +236,7 @@ mod tests {
}
}
assert!(s.model.results.iter().all(|r| r.error.is_none()));
let world = s.world_state().unwrap();
let world = s.world_state();
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
}
+1 -1
View File
@@ -419,7 +419,7 @@ fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: In
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
// A turn can start and end between two polls: then the conversation has its answer.
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
if idle && (seen_busy || answered || matches!(state, "exited" | "failed" | "auth" | "off") || began.elapsed() >= START_GRACE) {
return Ok((chat, false));
}
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
+26 -3
View File
@@ -40,6 +40,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
"unsupported": run.unsupported,
"rejected": run.rejected,
"boolean_fallbacks": run.boolean_fallbacks,
"boolean_reports_truncated": run.boolean_reports_truncated,
"panics": PANICS.swap(0, std::sync::atomic::Ordering::SeqCst),
});
if run.outcome.is_bug() {
@@ -189,9 +191,23 @@ fn summarise(results: &[Value], took: Duration) -> String {
let mut boolean: BTreeMap<String, usize> = BTreeMap::new();
let mut at_step: BTreeMap<String, usize> = BTreeMap::new();
let mut unsupported: BTreeMap<String, usize> = BTreeMap::new();
let mut fallbacks: BTreeMap<String, [u64; 2]> = BTreeMap::new();
let mut truncated = [0usize; 2];
let mut steps = 0;
let (mut dp, mut bp) = (0, 0);
for v in results {
for (i, group) in ["design", "boolean"].into_iter().enumerate() {
if v[group]["boolean_reports_truncated"].as_bool().unwrap_or(false) {
truncated[i] += 1;
}
if let Some(counts) = v[group]["boolean_fallbacks"].as_object() {
for (strategy, count) in counts {
if let Some(total) = fallbacks.entry(strategy.clone()).or_default().get_mut(i) {
*total = total.saturating_add(count.as_u64().unwrap_or(0));
}
}
}
}
dp += usize::from(v["design"]["panics"].as_u64().unwrap_or(0) > 0);
bp += usize::from(v["boolean_panics"].as_u64().unwrap_or(0) > 0);
let k = if v.get("timeout").is_some() {
@@ -230,6 +246,11 @@ fn summarise(results: &[Value], took: Duration) -> String {
for (k, c) in &boolean {
s += &format!("- {k}: {}\n", pct(*c));
}
s += "\nSuccessful Boolean retries (events, including nested operations):\n";
for (strategy, [designs, checks]) in &fallbacks {
s += &format!("- {strategy}: designs {designs}, boolean checks {checks}\n");
}
s += &format!("- Capped reports: designs {}, boolean checks {}\n", truncated[0], truncated[1]);
s
}
@@ -238,13 +259,15 @@ mod tests {
use super::*;
#[test]
fn report_counts_outcomes_and_timeouts() {
fn report_counts_retry_events_separately_and_marks_incomplete_logs() {
let results = [
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":2}},"boolean":{"outcome":"ok","boolean_fallbacks":{"shifted_operands":3},"boolean_reports_truncated":true}}),
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":1}},"boolean":{"outcome":"ok"}}),
json!({"timeout":90}),
];
let text = summarise(&results, Duration::ZERO);
assert!(text.contains("shifted_operands: designs 3, boolean checks 3"), "{text}");
assert!(text.contains("Capped reports: designs 0, boolean checks 1"), "{text}");
assert!(text.contains("timeout: 1 (33.3 %)"), "{text}");
}
}
File diff suppressed because it is too large Load Diff
+29 -112
View File
@@ -1,21 +1,20 @@
//! SolidCraft headless command line.
//!
//! ```text
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors] [--validation MODE]
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
//! solidcraft-cli 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]
//! 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
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
//! the standard Assistant model test (#4851) on a running app
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes] [--quality LEVEL]
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
@@ -32,11 +31,9 @@ mod bench_agent;
mod bench_assistant;
mod crashes;
mod fuzz;
mod kernel_cases;
mod logger;
mod oracle;
mod preview;
mod profiling;
mod recipe;
mod render;
mod spatial;
@@ -59,19 +56,19 @@ struct Command {
}
/// `run`'s lines in the usage.
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
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";
/// `eval`'s lines in the usage.
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.solidcraft|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
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.
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
/// `bench-agent`'s lines in the usage.
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.sc] [--script-out script.json]\n";
/// `bench-assistant`'s lines in the usage.
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
/// `snapshot`'s lines in the usage.
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27] [--quality coarse|normal|fine|ultra]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON;\n --quality: the display quality the bodies are meshed at, as the app's setting (default normal))\n";
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.sc|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
/// `render`'s lines in the usage.
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
/// `exec`'s lines in the usage.
@@ -82,13 +79,11 @@ const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
/// `oracle`'s lines in the usage.
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
/// `kernel-cases`'s lines in the usage.
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\n";
/// `fuzz`'s lines in the usage.
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
/// `mcp`'s lines in the usage.
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
/// `crashes`'s lines in the usage.
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
/// `licences`'s lines in the usage.
@@ -121,7 +116,6 @@ const COMMANDS: &[Command] = &[
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
Command { name: "kernel-cases", usage: USAGE_KERNEL_CASES, listed: true },
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
Command { name: "mcp", usage: USAGE_MCP, listed: true },
@@ -137,19 +131,6 @@ const COMMANDS: &[Command] = &[
fn main() -> ExitCode {
let mut args: Vec<String> = std::env::args().skip(1).collect();
logger::init(&mut args);
// `--profile` / `--profile-out` (run, eval, bench-open) or SOLIDCRAFT_PROFILE (any command):
// record from here, and print the table when the command ends.
let profiling = match args.first().map(String::as_str) {
Some("run" | "eval" | "inspect" | "bench-open") => profiling::start(&args[1..]),
_ => profiling::start(&[]),
};
let profiling = match profiling {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let r = match args.first().map(String::as_str) {
// Asked-for help is not an error: the command's usage on stdout, exit 0. It is read
// before the command runs, so `--help` never starts a run (a fuzz run, a server).
@@ -183,7 +164,6 @@ fn main() -> ExitCode {
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
}),
Some("oracle") => oracle::run(&args[1..]),
Some("kernel-cases") => kernel_cases::run(&args[1..]),
Some("fuzz") => fuzz::run(&args[1..]),
Some("fuzz-one") => fuzz::one(&args[1..]),
Some("mcp") => cmd_mcp(&args[1..]),
@@ -216,11 +196,6 @@ fn main() -> ExitCode {
}
_ => Err(usage()),
};
// The table also when the command failed: where it spent its time before failing.
let r = match (r, profiling.map(profiling::Profiling::finish)) {
(Ok(()), Some(Err(e))) => Err(e),
(r, _) => r,
};
match r {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
@@ -278,15 +253,8 @@ pub fn load(path: &str) -> Result<Session, String> {
/// `load`, with scripts and recipes run at the parameter-check level `check`, which the
/// session keeps.
fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
load_with(path, check, solidcraft_engine::kernel::Validation::default())
}
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
/// the session keeps.
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
let mut s = Session::default();
s.param_check = check;
s.set_validation(validation);
if solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path) {
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
return Ok(s);
@@ -294,7 +262,7 @@ fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validatio
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
if solidcraft_engine::io::is_design_container(&bytes) {
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
let mut session = Session::opened_at_validating(doc, None, Some(path), validation);
let mut session = Session::opened_at(doc, None, Some(path));
session.param_check = check;
return Ok(session);
}
@@ -340,16 +308,6 @@ fn positional(args: &[String]) -> Option<&String> {
None
}
/// Is `path` a command script (or a recipe), not a design: JSON that is no saved design.
fn is_script(path: &str) -> bool {
let Ok(bytes) = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES) else { return false };
if solidcraft_engine::io::is_design_container(&bytes) {
return false;
}
serde_json::from_slice::<Value>(&bytes)
.is_ok_and(|v| !v.get("format").and_then(Value::as_str).is_some_and(|f| solidcraft_engine::doc::format::version_of(f).is_some()))
}
/// `--json-out FILE`: where the JSON a command prints goes instead of stdout.
fn json_out(args: &[String]) -> Result<Option<&str>, String> {
if !args.iter().any(|a| a == "--json-out") {
@@ -381,34 +339,21 @@ 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", "--profile-out", "--validation"],
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
true,
)?;
let validation = validation_flag("run", args)?;
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
return Err(usage());
}
let want_results = args.iter().any(|a| a == "--results");
// `--profile` records the profiler's spans (the table goes to stderr, `profiling.rs`); for a
// script it also times each command and splits out its kernel time, implying `--timings`.
let script = path.is_some_and(|p| is_script(p));
let profile = script && args.iter().any(|a| a == "--profile" || a == "--profile-out");
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
let profile = args.iter().any(|a| a == "--profile");
let timings = profile || args.iter().any(|a| a == "--timings");
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_with(b, solidcraft_engine::schema::ParamCheck::Off, validation)?,
None => {
let mut s = Session::default();
s.set_validation(validation);
s
}
Some(b) => load(b)?,
None => Session::default(),
};
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
let v = read_json(path.ok_or(needs)?)?;
@@ -423,7 +368,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
};
(s, Some(out))
} else {
(open_validating(base, path, validation)?, None)
(open(base, path)?, 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
@@ -450,12 +395,6 @@ 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> {
open_validating(base, path, solidcraft_engine::kernel::Validation::default())
}
/// [`open`], the features computed under `validation`.
fn open_validating(base: Option<&str>, path: Option<&String>, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
let load = |p: &str| load_with(p, solidcraft_engine::schema::ParamCheck::Off, validation);
match (base, path) {
(Some(b), Some(p)) => {
let mut s = load(b)?;
@@ -468,15 +407,6 @@ fn open_validating(base: Option<&str>, path: Option<&String>, validation: solidc
}
}
/// `--validation MODE` (`diagnostics.validation`): the default, each_feature, when not given.
fn validation_flag(cmd: &str, args: &[String]) -> Result<solidcraft_engine::kernel::Validation, String> {
match flag(args, "--validation") {
Some(m) => solidcraft_engine::kernel::Validation::parse(m)
.ok_or_else(|| format!("{cmd}: --validation must be each_step, each_feature or off, not `{m}`")),
None => Ok(solidcraft_engine::kernel::Validation::default()),
}
}
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
@@ -514,7 +444,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", "--profile-out"], &["--profile"], true)?;
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
let mut s = open(flag(args, "--in"), positional(args))?;
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())?;
@@ -596,7 +526,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
known_args(
"snapshot",
args,
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section", "--quality"],
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
&["--no-grid", "--no-axes"],
true,
)?;
@@ -609,16 +539,6 @@ 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)?);
// The display quality the bodies are meshed at, as the app's Display Quality setting: set
// before the design opens, so every display plan is made at it.
if let Some(q) = flag(args, "--quality") {
use solidcraft_engine::doc::display_quality::{DisplayQuality, set_current};
let level = DisplayQuality::parse(q).ok_or_else(|| {
let ids: Vec<&str> = DisplayQuality::ALL.iter().map(|q| q.id()).collect();
format!("--quality must be one of {}, not `{q}`", ids.join(", "))
})?;
set_current(level);
}
let mut s = open(flag(args, "--in"), positional(args))?;
// How the bodies are drawn and where the model is cut, through the commands the app's
// Appearance dialog and Section Analysis run (`preview`).
@@ -638,12 +558,9 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
}
(None, None) => {}
}
let cam = match s.camera {
Some(cam) => cam,
None => solidcraft_engine::view::home_camera(&s).map_err(|e| e.to_string())?,
};
let cam = s.camera.unwrap_or_else(|| solidcraft_engine::view::home_camera(&s));
let guides = Guides { grid: !args.iter().any(|a| a == "--no-grid"), axes: !args.iter().any(|a| a == "--no-axes") };
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides).map_err(|e| e.to_string())?;
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides);
let png = solidcraft_engine::capture::png(&scene, &cam, w, h)?;
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
@@ -880,7 +797,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
let r = s.execute(id, p).map_err(|e| format!("{id}: {e}"))?;
let ms = t.elapsed().as_secs_f64() * 1000.0;
let t = std::time::Instant::now();
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
s.world_state().warm_meshes(|| true);
let view_ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", json!({"step": "op", "command": id, "ms": ms, "view_ms": view_ms}));
Ok(r)
@@ -900,7 +817,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
/// Separate file reading, JSON decoding/repair, timeline evaluation and display meshing.
pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]")?;
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.sc | part.step> [--parse-only]")?;
let t = std::time::Instant::now();
let step = solidcraft_engine::io::is_step_path(path);
let limit = if step { solidcraft_engine::io::MAX_STEP_BYTES } else { solidcraft_engine::io::MAX_DESIGN_BYTES };
@@ -936,7 +853,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
let t = std::time::Instant::now();
let st = s.world_state().map_err(|e| e.to_string())?;
let st = s.world_state();
let place_ms = t.elapsed().as_secs_f64() * 1000.0;
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
@@ -946,13 +863,13 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "place_ms": place_ms, "bodies": st.bodies.len(), "triangles": triangles})
);
let flag = |name: &str| args.iter().position(|a| a == name);
// `--save PATH`: save the opened design without its model cache (to PATH.plain.solidcraft) and
// `--save PATH`: save the opened design without its model cache (to PATH.plain.sc) and
// as the app does, with it, then read the cache back.
let s = match flag("--save").and_then(|i| args.get(i + 1)) {
Some(out) => {
let mut s = s;
// Without the cache first, for comparison.
let plain = format!("{out}.plain.solidcraft");
let plain = format!("{out}.plain.{}", solidcraft_engine::io::DESIGN_EXT);
let t = std::time::Instant::now();
s.execute("file.save_as", &json!({"path": plain, "cache": false})).map_err(|e| e.to_string())?;
let plain_ms = t.elapsed().as_secs_f64() * 1000.0;
@@ -1006,7 +923,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let t = std::time::Instant::now();
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
let st = s.world_state().map_err(|e| e.to_string())?;
let st = s.world_state();
let t = std::time::Instant::now();
st.warm_meshes(|| true);
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
+18 -18
View File
@@ -13,28 +13,28 @@ use crate::{read_json, recipe};
/// Default relative tolerance for volume and area (our curved-face tessellation is within ~2e-4).
pub const REL_TOL: f64 = 1e-3;
/// Default tolerance for centre of mass and bounding box, as a fraction of the bbox diagonal.
pub(crate) const ABS_FRAC: f64 = 1e-3;
const ABS_FRAC: f64 = 1e-3;
/// Chord tolerance used to triangulate our part for the surface-distance check (mm).
const MESH_TOL: f64 = 0.01;
/// Relative tolerance on the principal second moments of the two triangulations.
const INERTIA_REL: f64 = 2e-3;
/// What SolidCraft built: totals over every body.
pub(crate) struct Got {
pub(crate) body_count: usize,
pub(crate) volume: f64,
pub(crate) area: f64,
pub(crate) com: [f64; 3],
pub(crate) bbox: ([f64; 3], [f64; 3]),
pub(crate) faces: u64,
struct Got {
body_count: usize,
volume: f64,
area: f64,
com: [f64; 3],
bbox: ([f64; 3], [f64; 3]),
faces: u64,
}
pub(crate) fn v3(v: &Value) -> Option<[f64; 3]> {
fn v3(v: &Value) -> Option<[f64; 3]> {
let a = v.as_array()?;
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?])
}
pub(crate) fn totals(m: &Value) -> Got {
fn totals(m: &Value) -> Got {
let bodies = m["bodies"].as_array().cloned().unwrap_or_default();
let (mut volume, mut area, mut faces) = (0.0, 0.0, 0);
let mut moment = [0.0; 3];
@@ -57,21 +57,21 @@ pub(crate) fn totals(m: &Value) -> Got {
Got { body_count: bodies.len(), volume, area, com, bbox: (lo, hi), faces }
}
pub(crate) struct Checks {
pub(crate) list: Vec<Value>,
pub(crate) pass: bool,
struct Checks {
list: Vec<Value>,
pass: bool,
}
impl Checks {
pub(crate) fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
self.pass &= ok;
self.list.push(json!({"check": what, "got": got, "want": want, "ok": ok}));
}
pub(crate) fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
let ok = w.is_some_and(|w| g.is_finite() && (g - w).abs() <= rel * w.abs().max(1.0));
self.push(what, json!(g), json!(w), ok);
}
pub(crate) fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
let ok = w.is_some_and(|w| g.iter().zip(w).all(|(g, w)| g.is_finite() && (g - w).abs() <= tol));
self.push(what, json!(g), json!(w), ok);
}
@@ -186,8 +186,8 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
/// Our part as a triangle soup (curved faces refined onto the surface).
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
let mut ours: Vec<Tri> = Vec::new();
for b in &s.world_state().map_err(|e| e.to_string())?.bodies {
let m = b.body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
for b in &s.world_state().bodies {
let m = b.body.tessellate_with(MESH_TOL, true).map_err(|e| e.to_string())?;
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
for t in &m.triangles {
if let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) {
-50
View File
@@ -1,50 +0,0 @@
//! `--profile` and `--profile-out FILE` for `run`, `eval` and `bench-open` (and
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, SMLib's calls to the
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
use solidcraft_engine::profile;
/// Rows the table prints.
const TOP: usize = 30;
/// A recording that prints its table when finished.
pub struct Profiling {
trace: Option<String>,
}
/// Start recording when `args` (after the command) ask for it, or `SOLIDCRAFT_PROFILE` does.
pub fn start(args: &[String]) -> Result<Option<Profiling>, String> {
let trace = match args.iter().position(|a| a == "--profile-out") {
Some(i) => match args.get(i + 1) {
Some(f) if !f.starts_with("--") => Some(f.clone()),
_ => return Err("--profile-out needs a FILE".into()),
},
None => None,
};
let asked = trace.is_some() || args.iter().any(|a| a == "--profile");
if !asked && !solidcraft_engine::timing::profile_from_env() {
return Ok(None);
}
if !profile::COMPILED {
return Err("this solidcraft-cli was built without the profiler (its `profile` feature)".into());
}
profile::reset();
profile::set_enabled(true);
Ok(Some(Profiling { trace }))
}
impl Profiling {
/// Stop recording, print the table on stderr and write the trace.
pub fn finish(self) -> Result<(), String> {
profile::set_enabled(false);
eprint!("{}", profile::report().table(TOP));
if let Some(path) = self.trace {
let mut bytes = Vec::new();
profile::write_chrome_trace(&mut bytes).map_err(|e| format!("{path}: {e}"))?;
std::fs::write(&path, bytes).map_err(|e| format!("{path}: {e}"))?;
eprintln!("profile: trace written to {path} (open it in chrome://tracing or ui.perfetto.dev)");
}
Ok(())
}
}
+9 -17
View File
@@ -92,7 +92,7 @@ fn designs() -> Vec<(String, PathBuf)> {
add("regressions", root.join("examples/regressions"), "json");
add("demo", root.join("examples/demo"), "json");
add("typical", root.join("tests/action_matrix/designs"), "json");
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
add("demo", root.join("tests/action_matrix/designs"), solidcraft_engine::io::DESIGN_EXT);
let mut cases: Vec<PathBuf> =
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
cases.sort();
@@ -105,7 +105,7 @@ fn designs() -> Vec<(String, PathBuf)> {
/// Build a design from its file.
fn load(path: &Path) -> Result<Session, String> {
let mut s = Session::default();
if path.extension().is_some_and(|x| x == "solidcraft") {
if solidcraft_engine::io::is_design_container_path(path) {
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
return Ok(s);
}
@@ -196,7 +196,7 @@ fn round3(x: f64) -> f64 {
/// Every action on every entity of the design's final bodies, in a fixed order.
fn actions(s: &Session) -> Vec<Action> {
let st = s.world_state().unwrap();
let st = s.world_state();
let placed = s.placed_document();
let paths: BTreeMap<String, Vec<u64>> =
placed.body_frames_with_source(&s.model.state()).into_iter().map(|(world, _, _, path, _)| (world, path)).collect();
@@ -1338,9 +1338,7 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
fn a_round_on_a_knuckle_cap_circle() {
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
@@ -1360,9 +1358,7 @@ fn a_round_on_a_knuckle_cap_circle() {
#[test]
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1379,9 +1375,7 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
#[test]
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1396,9 +1390,7 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
#[test]
fn a_hole_square_to_a_45_degree_flange() {
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
let volume = |s: &Session| {
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
};
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
let before = volume(&s);
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
@@ -1566,7 +1558,7 @@ fn sketches_on_loft_and_sweep_faces() {
#[test]
fn press_pull_on_the_pi_case_floor() {
reproduces(
Some("tests/action_matrix/designs/pi-case.solidcraft"),
Some("tests/action_matrix/designs/pi-case.sc"),
json!([{"command": "solid.press_pull", "params": {"face": [30, 20, 0], "distance": 1}}]),
);
}
@@ -1736,7 +1728,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
st.bodies
.iter()
.map(|b| {
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
let mesh = b.body.tessellate(0.1).unwrap();
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
});
+2 -2
View File
@@ -26,8 +26,8 @@ fn assert_two_through_holes(session: &Session, stage: &str) {
let expected = 2.0 * PI * 4.0_f64.powi(2) * 6.0;
assert!((removed - expected).abs() < expected * 1e-3, "{stage}: the two holes remove {removed} mm³, expected {expected} mm³");
let before_mesh = uncut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let after_mesh = bracket.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let before_mesh = uncut.tessellate(0.01).unwrap();
let after_mesh = bracket.tessellate(0.01).unwrap();
let down = Vec3::new(0.0, 0.0, -1.0);
for y in [-15.0, -45.0] {
let center = Vec3::new(25.0, y, 1.0);
+4 -6
View File
@@ -1,12 +1,10 @@
# Design file corpus
Designs saved by older SolidCraft builds. `tests/design_corpus.rs` opens each one with the
current build and checks that it is upgraded to the current file format, that it evaluates
(a feature may fail only by asking for a reference it lost to be picked again; what it rebuilds
to is today's), and that damaged and hostile variants of it never panic. The `.expected.json`
next to each is `solidcraft-cli eval` of that file by the build that saved it (bodies, volumes,
areas, world bounding boxes), kept as a record of what that build made; no test compares with
it.
current build and checks that it evaluates as it did when saved (the `.expected.json` next to
it is `solidcraft-cli eval` of that file by the build that saved it: bodies, volumes, areas,
world bounding boxes), that it is upgraded to the current file format, and that damaged and
hostile variants of it never panic.
| File | Saved by | Era |
|---|---|---|
@@ -90,6 +90,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.131943,
@@ -100,6 +104,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 1.0812709999999999,
@@ -110,6 +118,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.450002,
@@ -120,6 +132,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 5.191514000000001,
@@ -54,6 +54,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 1.178957,
@@ -64,6 +68,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 83.24342399999999,
@@ -74,6 +82,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.150889,
@@ -84,6 +96,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 94.465481,
@@ -94,6 +110,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 50.426736,
@@ -104,6 +124,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 40.383005,
@@ -114,6 +138,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 7,
"ms": 59.041461000000005,
@@ -124,6 +152,18 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [
{
"face_shift_mm": 0.0,
"rotation_rad": 0.0,
"strategy": "planar_fallback",
"tolerance_mm": 0.0,
"translation_mm": 0.0
}
],
"truncated": false
},
"error": null,
"id": 8,
"ms": 562.5477970000001,
@@ -131,9 +171,13 @@
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
"warning": "Boolean retry: exact planar fallback (no displacement)"
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 9,
"ms": 89.56076,
@@ -144,6 +188,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 10,
"ms": 65.330634,
@@ -154,6 +202,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 11,
"ms": 97.229403,
@@ -99,6 +99,10 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.204585,
@@ -109,6 +113,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 2.318662,
@@ -119,6 +127,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.109196,
@@ -129,6 +141,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 0.338859,
@@ -139,6 +155,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 0.312259,
@@ -149,6 +169,10 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 0.23144699999999999,
+71 -39
View File
@@ -1,6 +1,5 @@
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open, are upgraded to
//! the current format and evaluate (what they rebuild to today is today's, not what the build
//! that saved them made); damaged and hostile design files never panic.
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open and evaluate as
//! they did when they were saved; damaged and hostile design files never panic.
use std::path::{Path, PathBuf};
@@ -22,24 +21,67 @@ fn open(path: &Path) -> Session {
s
}
/// Every corpus design opens, is upgraded to the current format, evaluates, and saves and reads
/// back the same. A feature may fail only by asking for a reference it lost to be picked again
/// (a name saved before format 14 finds nothing today).
/// Differences that are deliberate behaviour changes made before the file format had versions
/// (no migration can tell those files apart): (file, check, why).
const KNOWN: [(&str, &str, &str); 2] = [
(
"m0-revolve-primitives",
"Body1 bbox",
"partial revolves turn toward the sketch normal since ac43365 (as most parametric CAD tools do); M0 turned them the other way",
),
("components-arm", "body_count", "Measure counted only the root component's bodies then; it measures every placed body now"),
];
fn known(file: &str, check: &str) -> bool {
KNOWN.iter().any(|(f, c, _)| *f == file && *c == check)
}
/// Volumes and areas then came from coarser tessellation (and curved booleans and fillets have
/// become exact since): the same part within this relative tolerance (the oracle's).
const REL: f64 = 1e-3;
fn close(a: f64, b: f64, rel: f64) -> bool {
(a - b).abs() <= rel * b.abs().max(1.0)
}
#[test]
fn old_designs_open_upgrade_and_evaluate() {
fn old_designs_open_and_evaluate_as_saved() {
let mut problems = Vec::new();
for p in corpus() {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let want: Value = serde_json::from_slice(&std::fs::read(p.with_extension("expected.json")).unwrap()).unwrap();
let mut s = open(&p);
for r in &s.model.results {
if let Some(e) = r.error.as_deref().filter(|e| solidcraft_engine::doc::reselect_parts(e).is_none()) {
if let Some(e) = &r.error {
problems.push(format!("{name}: feature {}: {e}", r.id));
}
}
assert_eq!(s.doc.format, solidcraft_engine::doc::format::format_string(solidcraft_engine::doc::format::FORMAT), "{name}: upgraded");
let got = s.execute("inspect.measure", &json!({})).unwrap();
if got["body_count"].as_u64().unwrap_or(0) == 0 {
problems.push(format!("{name}: no bodies"));
if got["body_count"] != want["body_count"] && !known(&name, "body_count") {
problems.push(format!("{name}: {} bodies, saved with {}", got["body_count"], want["body_count"]));
}
for w in want["bodies"].as_array().into_iter().flatten() {
let bname = w["name"].as_str().unwrap_or_default();
let Some(g) = got["bodies"].as_array().into_iter().flatten().find(|g| g["name"] == w["name"]) else {
problems.push(format!("{name}: body {bname} missing"));
continue;
};
for k in ["volume_mm3", "area_mm2"] {
let (a, b) = (g[k].as_f64().unwrap_or(f64::NAN), w[k].as_f64().unwrap_or(f64::NAN));
if !close(a, b, REL) {
problems.push(format!("{name}: {bname} {k} {a} (saved {b})"));
}
}
if known(&name, &format!("{bname} bbox")) {
continue;
}
for (end, i) in [("min", 0), ("min", 1), ("min", 2), ("max", 0), ("max", 1), ("max", 2)] {
let (a, b) = (g["bbox"][end][i].as_f64().unwrap_or(f64::NAN), w["bbox"][end][i].as_f64().unwrap_or(f64::NAN));
if (a - b).abs() > 1e-3 * b.abs().max(1.0) {
problems.push(format!("{name}: {bname} bbox {end}[{i}] {a} (saved {b})"));
}
}
}
// Saved again, it reads back the same.
let again = solidcraft_engine::doc::Document::from_json(&s.doc.to_json()).unwrap();
@@ -52,48 +94,40 @@ fn old_designs_open_upgrade_and_evaluate() {
}
/// Names saved before format 14 were worked out from geometry, pieces of a split face numbered by
/// where their centres lay (`F2:top#2`). They open marked as such and find no face or edge today:
/// each feature holding one fails, asking for its reference to be picked again, and the rest of
/// the design builds.
/// where their centres lay (`F2:top#2`). They keep naming the faces they named: marked as such,
/// they are found as the build that saved them found them, without new notes.
#[test]
fn names_saved_before_exact_history_ask_for_their_faces_again() {
fn names_saved_before_exact_history_find_the_faces_they_named() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/naming-split-pieces.solidcraft");
let s = open(&path);
let feature = |name: &str| s.doc.features.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name}"));
assert_eq!(feature("Round").edge_names, vec!["?F2:side:l2|F2:top#2".to_string()]);
assert_eq!(feature("Bevel").edge_names, vec!["?F2:side:l4|F2:top#1".to_string()]);
assert_eq!(feature("Raise").face_names, vec!["?F2:top#1".to_string()]);
let asked = |name: &str| {
let r = s.model.result(feature(name).id).unwrap_or_else(|| panic!("{name}"));
r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts).map(|(f, reference)| (f.to_string(), reference.to_string()))
};
assert_eq!(asked("Round"), Some(("Round".into(), "?F2:side:l2|F2:top#2".into())));
assert_eq!(asked("Bevel"), Some(("Bevel".into(), "?F2:side:l4|F2:top#1".into())));
assert_eq!(asked("Raise"), Some(("Raise".into(), "?F2:top#1".into())));
for r in s.model.results.iter().filter(|r| !["Round", "Bevel", "Raise"].contains(&r.name.as_str())) {
assert!(r.error.is_none(), "{}: {:?}", r.name, r.error);
for r in &s.model.results {
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
}
assert!(!s.model.state().bodies.is_empty());
// The raised piece is the left one, as saved (the stored point is on it).
let state = s.model.state();
let solid = |p: [f64; 3]| state.bodies.iter().any(|b| b.body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2])));
assert!(solid([7.0, 15.0, 11.0]) && !solid([32.0, 15.0, 11.0]));
}
/// Body names saved before format 14 named the pieces of a split body in the kernel's shell
/// order: such a design opens with every feature marked `shell_piece_order` and evaluates, each
/// body name it stores naming a piece. (Which piece takes which name is not promised to be as it
/// was saved.) Its fillet and chamfer hold edge names saved then, which find nothing today: they
/// ask for their edges to be picked again.
/// Body names saved before format 14 name the pieces of a split body in the kernel's shell order.
/// The features after the split that refer to a piece by its body name still find that piece:
/// the far one stays `Body1` (chamfered), the near one `Body1 (1)` (filleted, coloured).
#[test]
fn body_names_saved_before_exact_history_open_marked_and_evaluate() {
fn body_names_saved_before_exact_history_keep_their_pieces() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/split-body-pieces.solidcraft");
let s = open(&path);
assert!(s.doc.features.iter().all(|f| f.shell_piece_order), "every saved feature keeps the shell order");
for r in &s.model.results {
let asked = r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts);
assert!(r.error.is_none() || asked.is_some_and(|(_, reference)| reference.starts_with('?')), "{}: {:?}", r.name, r.error);
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
}
let state = s.model.state();
for name in ["Body1", "Body1 (1)"] {
assert!(state.bodies.iter().any(|b| b.name == name), "{name}");
}
let piece = |name: &str| state.bodies.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("{name}"));
let solid = |name: &str, p: [f64; 3]| piece(name).body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2]));
assert!(solid("Body1", [20.0, 24.0, 5.0]) && solid("Body1 (1)", [20.0, 6.0, 5.0]));
assert!(s.doc.appearances.bodies.contains_key("Body1 (1)"));
}
@@ -139,7 +173,6 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
for p in corpus() {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let mut s = open(&p);
let failed_on_open: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
let state = s.model.state();
let placed = s.placed_document();
let mut picks = Vec::new();
@@ -147,7 +180,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
let Some(body) = state.bodies.iter().find(|b| b.definition_name() == definition && placed.model_body_component(b) == component) else {
continue;
};
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let mesh = body.body.tessellate(0.01).unwrap();
for index in 0..body.body.face_count() {
if body.body.planar_face(index).is_none() {
continue;
@@ -191,8 +224,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
let known = known_without_attachment(&name, &body, normal, distance);
out.push((name.clone(), body, index, known, problem));
}
let failed: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
assert_eq!(failed, failed_on_open, "{name}: undo leaves the features that failed on opening, and only those, failed");
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{name}: left with errors after undo");
}
out
}
+1 -54
View File
@@ -63,7 +63,7 @@ fn model(name: &str, s: &Session) -> Value {
.iter()
.map(|r| json!({"id": r.id, "name": r.name, "error": r.error, "warning": r.warning, "skipped": r.skipped, "state": state(name, &r.state)}))
.collect();
json!({"results": results, "world": state(name, &s.world_state().unwrap())})
json!({"results": results, "world": state(name, &s.world_state())})
}
/// The same edit on both: a parameter's value when the design has one, else a new body.
@@ -288,56 +288,3 @@ 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 saved model holds the results of the body check mode it was evaluated under: opened under
/// the same mode it is taken as it is, under another the design is evaluated again.
#[test]
fn a_model_saved_under_another_check_mode_is_evaluated_again() {
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 path = temp("mode");
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
let same = open(&path);
assert_eq!(same.model.validation(), built.model.validation());
assert_eq!(same.model.recomputed, 0, "taken from the file");
let mut other = Session::default();
other.set_validation(solidcraft_engine::kernel::Validation::EachStep);
assert_ne!(other.validation(), built.model.validation());
other.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(other.validation(), solidcraft_engine::kernel::Validation::EachStep);
assert!(other.model.recomputed > 0, "evaluated again under its own mode");
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A body made by rounds keeps, in the saved model, the rounds and the body they were made on: a
/// later round at the corner where they meet blends them again with it whether the design was
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
/// rounded at once make, 10 faces and 7928.743041 mm³, the same body as the design evaluated anew.
#[test]
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let mut built = Session::default();
built.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Box"})).unwrap();
built.execute("solid.fillet", &json!({"edges": [[20, 10, 20], [10, 0, 20]], "radius": 2})).unwrap();
let path = temp("later-round");
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
let bytes = std::fs::read(&path).unwrap();
let mut cached = open(&path);
assert_eq!(cached.model.last_recomputed, 0, "opened without evaluating");
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
for s in [&mut cached, &mut rebuilt] {
s.execute("solid.fillet", &json!({"edges": [[20, 0, 10]], "radius": 3})).unwrap();
let st = s.model.state();
let b = &st.body("Box").unwrap().body;
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
assert_eq!(b.face_count(), 10);
assert!((v - 7928.743041).abs() < 1e-6, "{v}");
}
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
-403
View File
@@ -1,403 +0,0 @@
//! Every design of the corpus loads, two builds of it name its faces and edges alike, and a
//! name it stores that no longer finds its face or edge fails its feature. The stored names are
//! the fillet and chamfer edges, the faces a feature picks, the face a sketch sits on and the
//! sheet metal edges; one that finds nothing (`gone`, a sketch's face `lost`) must leave its
//! feature failed (it asks for the reference to be picked again, never takes another face or
//! edge), and a design that no longer loads fails the test. A build's names are each face's and
//! edge's name with where it is (its area and centre, its length and middle); the second build
//! must give the same, and its stored names must find the same faces and edges.
//!
//! Face and edge indices are left out: they are the kernel's order, which a rebuild need not
//! keep (a fillet's or a boolean's faces come out in another order from one build to the next).
//! How many faces a body has and what each is called is the kernel's business, not this test's.
//!
//! The corpus is the action matrix's (the old design files in `tests/corpus`, the examples, the
//! demo and typical designs in `tests/action_matrix/designs`, the reference recipes), the sample
//! designs, the engine's design fixtures and the older designs kept by the hole matrix, taper
//! and projection tests.
use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::{FaceAttachmentStatus, Feature, FeatureKind, ModelBody, ModelState, PlaneRef, naming};
use solidcraft_engine::geom::Vec3;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
}
/// Where a design comes from.
enum Source {
File(PathBuf),
Sample(fn() -> Value),
}
/// Every design of the corpus: (`<group>/<design>`, where it comes from).
fn designs() -> Vec<(String, Source)> {
let root = root();
let mut out = Vec::new();
let mut add = |prefix: &str, dir: PathBuf, ext: &str| {
let Ok(rd) = std::fs::read_dir(&dir) else { return };
let mut v: Vec<PathBuf> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == ext)).collect();
v.sort();
for p in v {
let stem = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
out.push((format!("{prefix}/{stem}"), Source::File(p)));
}
};
add("corpus", root.join("apps/solidcraft-cli/tests/corpus"), "solidcraft");
add("examples", root.join("examples"), "json");
add("examples", root.join("examples/acceptance"), "json");
add("regressions", root.join("examples/regressions"), "json");
add("demo", root.join("examples/demo"), "json");
add("typical", root.join("tests/action_matrix/designs"), "json");
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
add("fixtures", root.join("crates/engine/src/tests/fixtures"), "solidcraft");
add("hole_matrix", root.join("tests/hole_matrix/old-0.9"), "json");
add("taper", root.join("tests/taper/before-1014"), "json");
add("projection", root.join("tests/projection/before-1508"), "json");
let mut cases: Vec<PathBuf> =
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
cases.sort();
for c in cases {
out.push((format!("oracle/{}", c.file_name().unwrap_or_default().to_string_lossy()), Source::File(c.join("recipe.json"))));
}
for sample in solidcraft_engine::sample::samples() {
let stem: String = sample.name.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-");
out.push((format!("samples/{stem}"), Source::Sample(sample.script)));
}
out
}
/// Build a design: a design file (also a design saved as JSON) is opened, a script is run.
fn load(source: &Source) -> Result<Session, String> {
let mut s = Session::default();
let script = match source {
Source::Sample(script) => script(),
Source::File(path) if path.extension().is_some_and(|x| x == "solidcraft") => {
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
return Ok(s);
}
Source::File(path) if path.file_name().is_some_and(|n| n == "recipe.json") => {
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("recipe").arg(path).output().map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(format!("recipe: {}", String::from_utf8_lossy(&out.stderr).trim()));
}
serde_json::from_slice(&out.stdout).map_err(|e| e.to_string())?
}
Source::File(path) => {
let v: Value = serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
if v.get("features").is_some() {
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
return Ok(s);
}
match v.get("steps").and_then(Value::as_array) {
// The demo's steps: an MCP `execute` each.
Some(steps) => {
json!({"commands": steps.iter().map(|st| json!({"command": st["command"], "params": st["params"]})).collect::<Vec<_>>()})
}
None => v,
}
}
};
s.run_script(&script).map_err(|e| e.to_string())?;
if s.active_sketch.is_some() {
let _ = s.execute("sketch.finish", &json!({}));
}
Ok(s)
}
/// What decides a name beyond its text: `[geometric]` when it was worked out from geometry, not
/// exact history.
fn flags(list: &[(bool, &str)]) -> String {
let set: Vec<&str> = list.iter().filter(|(on, _)| *on).map(|(_, f)| *f).collect();
if set.is_empty() { String::new() } else { format!(" [{}]", set.join(", ")) }
}
/// Where each named face and edge of a body is, by name, sorted: a face's area and centre, an
/// edge's length and middle, from the body's mesh. Two builds that name their faces alike give
/// the same list.
fn placed_names(b: &ModelBody) -> Vec<(String, [f64; 4])> {
if b.body.is_mesh() {
return Vec::new();
}
let m = b.mesh();
let faces = naming::face_names(b);
let mut sums = vec![[0.0f64; 4]; faces.len()];
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let [p, q, r] = t.map(|i| m.positions.get(i as usize).copied().unwrap_or(Vec3::ZERO));
let area = (q - p).cross(r - p).len() / 2.0;
let c = (p + q + r) / 3.0;
if let Some(s) = sums.get_mut(*f as usize) {
*s = [s[0] + area, s[1] + area * c.x, s[2] + area * c.y, s[3] + area * c.z];
}
}
let mut out: Vec<(String, [f64; 4])> = faces
.iter()
.zip(&sums)
.map(|(n, s)| {
let a = s[0].max(1e-300);
(format!("{} face {n}", b.name), [s[0], s[1] / a, s[2] / a, s[3] / a])
})
.collect();
for (n, line) in naming::edge_names(b).iter().zip(&m.edges) {
let length: f64 = line.windows(2).map(|w| w[1].dist(w[0])).sum();
let mid = line.iter().fold(Vec3::ZERO, |acc, p| acc + *p) / (line.len().max(1) as f64);
out.push((format!("{} edge {n}", b.name), [length, mid.x, mid.y, mid.z]));
}
out.sort_by(|x, y| x.0.cmp(&y.0).then(x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal)));
out
}
fn face_label(st: &ModelState, body: usize, face: usize) -> String {
match st.bodies.get(body) {
Some(b) => format!("{} face {}", b.name, naming::face_names(b).get(face).map(String::as_str).unwrap_or("?")),
None => format!("body #{body} face #{face}"),
}
}
/// Every name feature `f` stores, with what it finds in the model before it, as a feature
/// rebuilding finds it, and whether it finds anything.
fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
let doc = &*s.doc;
let who = format!("ref {} ({})", f.name, f.id);
let before = s.model.state_before(f.id);
// Faces a feature picks by point and stores by name (shell, draft, hole, …).
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
if f.face_names.iter().any(|n| !n.is_empty()) {
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component).unwrap();
for (k, (p, n)) in f.kind.face_points().into_iter().zip(&f.face_names).enumerate() {
if n.is_empty() {
continue;
}
let (found, ok) = if explicit_emboss {
("not checked (a cylindrical emboss finds its face on its own body)".to_string(), true)
} else {
match naming::locate_face_on_body(&fst, f.kind.face_body(), n, p) {
Some(hit) => (format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])), true),
None => ("gone".into(), false),
}
};
out.push((format!("{who} face {k} `{n}` -> {found}"), ok));
}
}
// Edges a feature picks by point and stores by name.
if f.edge_names.iter().any(|n| !n.is_empty()) {
let edge = |out: &mut Vec<(String, bool)>, b: &ModelBody, k: usize, n: &str, p: Vec3| {
let (found, ok) = match naming::locate_edge(b, n, p) {
Ok(hit) => (
format!(
"{} edge {}{}",
b.name,
naming::edge_names(b).get(hit.edge).map(String::as_str).unwrap_or("?"),
flags(&[(!hit.verified, "geometric")])
),
true,
),
Err(e) => (format!("gone ({e})"), false),
};
out.push((format!("{who} edge {k} `{n}` -> {found}"), ok));
};
let named = |k: usize| f.edge_names.get(k).filter(|n| !n.is_empty());
match &f.kind {
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
let pts: Vec<Vec3> = solidcraft_engine::doc::plain(edges);
match solidcraft_engine::doc::blend_target(&before, body, &pts).ok().and_then(|i| before.bodies.get(i)) {
Some(b) => {
for (k, p) in pts.iter().enumerate() {
if let Some(n) = named(k) {
edge(out, b, k, n, *p);
}
}
}
None => out.push((format!("{who} edges: no body to round"), false)),
}
}
FeatureKind::SheetConvert { body, bends, .. } => match before.body(body) {
Some(b) => {
for (k, p) in solidcraft_engine::doc::plain::<Vec3, _>(bends).iter().enumerate() {
if let Some(n) = named(k) {
edge(out, b, k, n, *p);
}
}
}
None => out.push((format!("{who} bends: body `{body}` gone"), false)),
},
// Sheet metal edges are named by the sheet's panels, and found on its sheets.
FeatureKind::SheetFlange { .. } | FeatureKind::SheetHem { .. } => {
let pts: Vec<Vec3> = match &f.kind {
FeatureKind::SheetFlange { edges, .. } => edges.iter().map(|e| e.point.0).collect(),
FeatureKind::SheetHem { edges, .. } => solidcraft_engine::doc::plain(edges),
_ => Vec::new(),
};
for (k, p) in pts.iter().enumerate() {
let Some(n) = named(k) else { continue };
let on: Vec<&str> = before.sheets.iter().filter(|sh| sh.edge_by_name(n, *p).is_some()).map(|sh| sh.body.as_str()).collect();
let found = if on.is_empty() { "gone".to_string() } else { format!("sheet edge on {}", on.join(", ")) };
out.push((format!("{who} sheet edge {k} `{n}` -> {found}"), !on.is_empty()));
}
}
other => {
for (k, n) in f.edge_names.iter().enumerate().filter(|(_, n)| !n.is_empty()) {
out.push((format!("{who} edge {k} `{n}` -> not checked ({})", other.type_name()), true));
}
}
}
}
// The face a sketch sits on.
if let FeatureKind::Sketch { plane: PlaneRef::Face { name, anchor, at, .. }, .. } = &f.kind {
let mut ok = true;
let status = match doc.sketch_face_attachment(f.id, &s.model) {
FaceAttachmentStatus::Healthy { target, .. } => {
let faces = match before.body(&target.body) {
Some(b) => match doc.anchored_face_indices(&before, b, &target) {
Ok((indices, note)) => {
let names = naming::face_names(b);
let mut list: Vec<&str> = indices.iter().map(|i| names.get(*i).map(String::as_str).unwrap_or("?")).collect();
list.sort();
format!("{} faces {}{}", b.name, list.join(", "), flags(&[(note.is_some(), "changed cap")]))
}
Err(e) => {
ok = false;
format!("no faces ({e})")
}
},
None => {
ok = false;
format!("body `{}` gone", target.body)
}
};
format!("healthy: {faces}")
}
FaceAttachmentStatus::Lost { message, .. } => {
ok = false;
format!("lost: {message}")
}
FaceAttachmentStatus::Legacy { .. } => match name {
// The face its name finds, as the sketch finds it.
Some(n) => {
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component).unwrap();
match naming::locate_face(&fst, n, *at) {
Some(hit) => format!("by name: {}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])),
None => {
ok = false;
"by name: gone".into()
}
}
}
None => "by its pick".into(),
},
FaceAttachmentStatus::Sheet { .. } => "sheet face".into(),
FaceAttachmentStatus::Fixed { .. } => "fixed".into(),
};
let stored = match (anchor, name) {
(Some(a), _) => {
let kind =
a.target.certificate.as_ref().and_then(|c| serde_json::to_value(c).ok()).and_then(|v| v["kind"].as_str().map(String::from));
format!("`{}` on {} ({})", a.target.name, a.target.body, kind.unwrap_or_else(|| "no certificate".into()))
}
(None, Some(n)) => format!("`{n}`"),
(None, None) => "by its pick".into(),
};
out.push((format!("{who} plane {stored} -> {status}"), ok));
}
}
/// A build of a design: where its faces and edges are by name, and what every name it stores
/// finds.
struct Build {
names: Vec<(String, [f64; 4])>,
refs: Vec<(String, bool)>,
}
fn build(s: &Session) -> Build {
let names = s.model.state().bodies.iter().flat_map(placed_names).collect();
let mut refs = Vec::new();
for f in s.doc.features.iter().filter(|f| !f.suppressed) {
let from = refs.len();
record_refs(&mut refs, s, f);
// A name that finds nothing is sound when its feature fails: it asks for the reference
// to be picked again.
if s.model.result(f.id).is_some_and(|r| r.error.is_some()) {
refs.iter_mut().skip(from).for_each(|(_, ok)| *ok = true);
}
}
Build { names, refs }
}
/// Where two builds name their faces and edges apart (at most `cap` of them): a name only one
/// has, or a face or edge of that name somewhere else (1e-6 of its size, to the mesh's rounding).
fn differences(a: &Build, b: &Build, cap: usize) -> Vec<String> {
let mut out = Vec::new();
if a.names.len() != b.names.len() {
out.push(format!("{} named faces and edges, then {}", a.names.len(), b.names.len()));
}
for ((n, x), (m, y)) in a.names.iter().zip(&b.names) {
let scale = x.iter().chain(y).fold(1.0f64, |s, v| s.max(v.abs()));
if n != m || x.iter().zip(y).any(|(p, q)| (p - q).abs() > 1e-6 * scale) {
out.push(format!("{n} at {x:?}, then {m} at {y:?}"));
}
}
for ((r, _), (q, _)) in a.refs.iter().zip(&b.refs) {
if r != q {
out.push(format!("{r}, then {q}"));
}
}
if a.refs.len() != b.refs.len() {
out.push(format!("{} stored names, then {}", a.refs.len(), b.refs.len()));
}
out.truncate(cap);
out
}
/// Designs whose names differ from one build to the next today: (design, why). Their
/// differences are printed, not failed on, until the cause is fixed; a stored name that finds
/// nothing still fails.
const UNSTABLE: [(&str, &str); 1] = [(
"regressions/closed_sweep_torus_cut",
"the halves of each swept wall lie level along every axis, so which is `#1` and which `#2` follows the order the kernel gives the faces in, which changes from one build to the next",
)];
#[test]
fn corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone() {
let designs = designs();
let next = std::sync::atomic::AtomicUsize::new(0);
let results = std::sync::Mutex::new(Vec::new());
let workers = std::thread::available_parallelism().map_or(4, |n| n.get()).min(designs.len().max(1));
std::thread::scope(|scope| {
for _ in 0..workers {
scope.spawn(|| {
while let Some((name, source)) = designs.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
let got = load(source).and_then(|first| Ok((build(&first), build(&load(source)?))));
results.lock().unwrap_or_else(|e| e.into_inner()).push((name.clone(), got));
}
});
}
});
let mut results = results.into_inner().unwrap_or_else(|e| e.into_inner());
results.sort_by(|a, b| a.0.cmp(&b.0));
let mut problems = Vec::new();
for (name, got) in &results {
let (first, second) = match got {
Ok(builds) => builds,
Err(e) => {
problems.push(format!("{name}: does not load: {e}"));
continue;
}
};
let lost: Vec<&str> = first.refs.iter().filter(|(_, ok)| !ok).map(|(r, _)| r.as_str()).collect();
if !lost.is_empty() {
problems.push(format!("{name}: stored names that find nothing, in features that build:\n {}", lost.join("\n ")));
}
let apart = differences(first, second, 40);
if !apart.is_empty() {
match UNSTABLE.iter().find(|(design, _)| design == name) {
Some((_, why)) => eprintln!("{name} (not checked: {why}):\n {}", apart.join("\n ")),
None => problems.push(format!("{name}: two builds name it apart (first, then second):\n {}", apart.join("\n "))),
}
}
}
assert!(problems.is_empty(), "{} of {} design(s) of the corpus:\n{}", problems.len(), results.len(), problems.join("\n"));
}
+19 -15
View File
@@ -31,7 +31,7 @@ struct Topology {
/// Every solid body's topology, by name.
fn topology(s: &Session) -> BTreeMap<String, Topology> {
let st = s.world_state().unwrap();
let st = s.world_state();
st.bodies
.iter()
.filter(|b| !b.body.is_mesh())
@@ -75,12 +75,15 @@ fn designs() -> Vec<(String, Session)> {
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "solidcraft" || x == "json") && !p.to_string_lossy().ends_with(".expected.json"))
.filter(|p| {
(solidcraft_engine::io::is_design_container_path(p) || p.extension().is_some_and(|x| x == "json"))
&& !p.to_string_lossy().ends_with(".expected.json")
})
.collect();
files.sort();
for p in files {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
let s = if solidcraft_engine::io::is_design_container_path(&p) {
open(&p)
} else {
let mut s = Session::default();
@@ -139,16 +142,18 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
/// into strips along the openings' tops and bottoms.
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
/// face count after each feature, and each outer side (the four walls and the bottom) one face
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
/// wall at 24) into strips along the openings' tops and bottoms.
#[test]
fn pi_case_sides_stay_whole_at_both_heights() {
fn pi_case_faces_per_step_at_both_heights() {
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
const FACES: [(usize, usize); 13] =
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
let dir = scratch_dir("pi");
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.sc")).unwrap()).unwrap();
let at = |height: &str, file: &str| {
let mut d = design.clone();
for p in d["params"].as_array_mut().unwrap() {
@@ -169,13 +174,12 @@ fn pi_case_sides_stay_whole_at_both_heights() {
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
let mut steps = Vec::new();
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
for k in 2..=14 {
for (k, want) in FACES {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state().unwrap();
let st = t.world_state();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
// Each outer side: the faces in its plane.
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
let on = (0..body.body.face_count())
+6 -1
View File
@@ -145,7 +145,7 @@ fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting(
#[test]
fn cli_loads_both_design_encodings_by_contents() {
let dir = Scratch::new();
for extension in ["solidcraft", "solvecraft", "json"] {
for extension in ["sc", "solidcraft", "solvecraft", "json"] {
let saved = dir.0.join(format!("box.{extension}"));
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&saved).output().unwrap());
let bytes = std::fs::read(&saved).unwrap();
@@ -412,6 +412,11 @@ fn legacy_extension_and_format_tag_still_open() {
assert_eq!(body_count(&resaved), 2);
let reopened = solidcraft_engine::io::read_design(&std::fs::read(&resaved).unwrap()).unwrap();
assert!(reopened.to_json().contains("\"solidcraft/"));
// `.solidcraft` (before `.sc`) opens as `--in` and as the design to evaluate; `.sc` too.
let current = dir.0.join("current.sc");
json_output(cli().args(["run", "--in"]).arg(&resaved).arg(dir.script()).arg("--save").arg(&current).output().unwrap());
assert_eq!(body_count(&current), 3);
assert!(solidcraft_engine::io::is_design_container(&std::fs::read(&current).unwrap()), "a .sc file is a design container");
}
/// `snapshot` starts from `--in` as `run` does (its script runs on that design), and refuses an
+1 -4
View File
@@ -65,10 +65,7 @@ fn allow_errors_writes_anyway_and_a_saved_design_keeps_failing() {
let output = cli().arg("run").arg(dir.broken_script()).arg("--allow-errors").arg("--out").arg(&step).arg("--save").arg(&saved).output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let measured: Value = serde_json::from_slice(&output.stdout).unwrap();
// SMLib integrates the volume (a relative accuracy of 1e-4 asked for; a box comes out within
// a few units in the last place), so the box's 4000 mm³ is checked to 1e-9, not to the bit.
let volume = measured["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume - 4000.0).abs() < 4000.0 * 1e-9, "the box without its fillet: {volume}");
assert_eq!(measured["total"]["volume_mm3"], json!(4000.0), "the box without its fillet");
assert!(std::fs::metadata(&step).unwrap().len() > 0);
// The saved design still holds the failing feature: running it fails too, `eval` reports it.
let again = dir.0.join("again.step");
+4 -4
View File
@@ -171,9 +171,9 @@ fn a_saved_view_is_its_camera_and_a_standard_view_is_unchanged() {
let top = dir.snapshot(&script, "top.png", &["--view", "top"]);
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
let scene = solidcraft_engine::view::scene(&s, &cam);
assert_same(&top, &solidcraft_engine::capture::png(&scene, &cam, SIZE.0, SIZE.1).unwrap().bytes, "the home camera turned to the top");
assert_same(&top, &dir.mcp(&script, "mcp-top.png", json!({"view": "top"})), "the MCP top view");
assert_differs(&top, &saved, "top and the saved view");
@@ -335,9 +335,9 @@ fn a_forced_cpu_snapshot_is_the_cpu_rasterizers_and_says_so() {
assert!(stderr.contains("CPU rasterizer"), "{stderr}");
let mut s = Session::default();
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
let mut cam = solidcraft_engine::view::home_camera(&s);
cam.set_view(solidcraft_engine::render::StandardView::Top);
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
let scene = solidcraft_engine::view::scene(&s, &cam);
let cpu = solidcraft_engine::render::render_png(&scene, &cam, SIZE.0 as usize, SIZE.1 as usize).unwrap();
assert_eq!(std::fs::read(dir.0.join("cpu.png")).unwrap(), cpu);
}
@@ -1,5 +1,5 @@
//! Straight edges stay one edge after many cuts (#1049). The owner's design
//! (`tests/action_matrix/designs/crazy-geom-cuts.solidcraft`, saved by 0.10.0): five extrudes,
//! (`tests/action_matrix/designs/crazy-geom-cuts.sc`, saved by 0.10.0): five extrudes,
//! joins and cuts, over four sketches, three of them on faces of the body with the faces'
//! edges projected. Cuts used to leave the straight edges of the walls they sliced split at
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
@@ -14,7 +14,7 @@ use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::Vec3;
fn design() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.solidcraft")
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
}
fn open(path: &Path) -> Session {
@@ -48,11 +48,11 @@ fn key(v: Vec3) -> [i64; 3] {
fn straight_edges_are_one_edge_after_many_cuts() {
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
let st = s.world_state().unwrap();
let st = s.world_state();
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
assert_eq!(bodies.len(), 1, "{what}: one body");
let body = &bodies[0].body;
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
let mesh = body.tessellate(0.01).unwrap();
// Every edge is drawn as one polyline; a straight one as one segment.
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
for (i, e) in mesh.edges.iter().enumerate() {
+1 -8
View File
@@ -14,15 +14,8 @@ name = "solidcraft-web"
path = "src/main.rs"
[features]
default = ["smlib", "profile"]
default = []
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh
# wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH, crates/smlib/build.rs). SMLib
# is the only kernel, so without it the web app models nothing.
smlib = ["solidcraft-engine/smlib"]
# The profiler, compiled in so any build can say where its time goes (the Profile operations
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
[dependencies]
solidcraft-engine = { workspace = true }
+5 -5
View File
@@ -27,12 +27,12 @@
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
<meta name="theme-color" content="#343840">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/ai.storyteller.solidcraft.png">
<link rel="apple-touch-icon" href="icons/256x256/ai.storyteller.solidcraft.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/io.tangencylabs.solidcraft.png">
<link rel="apple-touch-icon" href="icons/256x256/io.tangencylabs.solidcraft.png">
<link data-trunk rel="copy-file" href="manifest.webmanifest" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png" data-target-path="icons/32x32" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png" data-target-path="icons/256x256" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png" data-target-path="icons/512x512" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/32x32" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/256x256" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/512x512" />
</head>
<body>
<canvas id="solidcraft_canvas"></canvas>
+3 -3
View File
@@ -9,8 +9,8 @@
"theme_color": "#343840",
"categories": ["productivity", "utilities"],
"icons": [
{ "src": "icons/32x32/ai.storyteller.solidcraft.png", "sizes": "32x32", "type": "image/png" },
{ "src": "icons/256x256/ai.storyteller.solidcraft.png", "sizes": "256x256", "type": "image/png" },
{ "src": "icons/512x512/ai.storyteller.solidcraft.png", "sizes": "512x512", "type": "image/png" }
{ "src": "icons/32x32/io.tangencylabs.solidcraft.png", "sizes": "32x32", "type": "image/png" },
{ "src": "icons/256x256/io.tangencylabs.solidcraft.png", "sizes": "256x256", "type": "image/png" },
{ "src": "icons/512x512/io.tangencylabs.solidcraft.png", "sizes": "512x512", "type": "image/png" }
]
}
+55 -8
View File
@@ -147,6 +147,21 @@ pub fn accept_list(extensions: &[&str]) -> String {
list.join(",")
}
/// The `accept` of a file input for `extensions` on this device (`apple_touch`: an iPhone or
/// iPad). The picker there offers only files of a type the system knows from the list and greys
/// out the rest, `.solidcraft` designs among them, since no system type has that extension
/// (#6977). So it offers every file there, and the opener checks what it gets.
pub fn input_accept(extensions: &[&str], apple_touch: bool) -> String {
if apple_touch { String::new() } else { accept_list(extensions) }
}
/// The browser runs on an iPhone or iPad (every browser there is WebKit), from its
/// `navigator.userAgent` and `navigator.maxTouchPoints`. iPadOS presents itself as a Mac; only its
/// touch screen tells them apart.
pub fn apple_touch(user_agent: &str, touch_points: i32) -> bool {
["iPhone", "iPad", "iPod"].iter().any(|d| user_agent.contains(d)) || (user_agent.contains("Macintosh") && touch_points > 1)
}
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
@@ -154,10 +169,15 @@ pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
}
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
if json { ("Plain JSON design", "application/json", ".json") } else { ("SolidCraft design", "application/octet-stream", ".solidcraft") }
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension). A
/// design is `.sc` unless the name already has an older design extension, which it keeps.
pub fn save_type(name: &str) -> (&'static str, &'static str, String) {
let ext = std::path::Path::new(name).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
match ext.as_deref() {
Some("json") => ("Plain JSON design", "application/json", ".json".into()),
Some(e) if solidcraft_engine::io::is_design_ext(e) => ("SolidCraft design", "application/octet-stream", format!(".{e}")),
_ => ("SolidCraft design", "application/octet-stream", format!(".{}", solidcraft_engine::io::DESIGN_EXT)),
}
}
/// Is this a cancelled dialog (the browser's `AbortError`)? Not an error to show.
@@ -249,7 +269,7 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"suggestedName".into(), &name.into());
let (description, mime, extension) = save_type(name);
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, extension));
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, &extension));
options
}
@@ -334,7 +354,9 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
let extensions = Array::new();
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
let designs = solidcraft_engine::io::DESIGN_EXTS.iter().map(|e| format!(".{e}"));
let others = [".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"].map(String::from);
for e in designs.chain(others) {
extensions.push(&e.into());
}
let accept = Object::new();
@@ -612,6 +634,8 @@ mod tests {
assert_eq!(route("/disk/18f-2a/Bracket.solidcraft"), WriteTo::Disk);
assert_eq!(route("/downloads/part.step"), WriteTo::Download);
assert_eq!(route("/upload/part.step"), WriteTo::Memory);
// The temporary folder (a screenshot without a path, #7192) stays in memory.
assert_eq!(route(&format!("{}/solidcraft-shot.png", solidcraft_engine::io::vfs::BROWSER_TEMP)), WriteTo::Memory);
}
#[test]
@@ -650,6 +674,7 @@ mod tests {
#[test]
fn only_design_opens_use_the_disk_dialog() {
assert!(opens_design(&[]));
assert!(opens_design(&["sc"]));
assert!(opens_design(&["solidcraft"]));
assert!(opens_design(&["solvecraft"]));
assert!(!opens_design(&["step", "stp"]));
@@ -671,11 +696,33 @@ mod tests {
assert_eq!(upload_limit(&["dxf"]), None);
}
/// An iPhone or iPad greys out a `.solidcraft` design the `accept` list names, as it knows no
/// type for that extension, so its inputs offer every file; other devices keep the list (#6977).
#[test]
fn file_inputs_on_an_iphone_or_ipad_offer_every_file() {
let iphone =
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1";
let mac = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15";
let chrome_ios = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.156 Mobile/15E148 Safari/604.1";
let chrome = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36";
assert!(apple_touch(iphone, 5));
assert!(apple_touch(chrome_ios, 5), "every browser on iOS has the same picker");
assert!(apple_touch(mac, 5), "an iPad presents itself as a Mac");
assert!(!apple_touch(mac, 0), "a Mac has no touch screen");
assert!(!apple_touch(chrome, 10), "a touch screen elsewhere filters as before");
assert!(!apple_touch("", 0), "hostile input is just not an iPhone");
let designs = ["solidcraft", "json", "step"];
assert_eq!(input_accept(&designs, true), "");
assert_eq!(input_accept(&designs, false), ".solidcraft,.json,.step");
}
#[test]
fn the_save_dialog_offers_the_type_of_the_name() {
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
assert_eq!(save_type("Plate.sc").2, ".sc");
assert_eq!(save_type("Plate.JSON").2, ".json");
assert_eq!(save_type("Plate").2, ".solidcraft");
assert_eq!(save_type("Plate").2, ".sc");
// A design opened under an older extension keeps it.
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
}
#[test]
+25 -12
View File
@@ -24,11 +24,13 @@ pub const OPFS: &str = disk::OPFS_PREFIX;
pub const DOWNLOADS: &str = disk::DOWNLOADS_PREFIX;
/// Files brought in from the computer.
pub const UPLOAD: &str = "/upload/";
/// The autosave of unsaved work (in OPFS: `recovery.solidcraft` and `recovery.json`).
const RECOVERY: &str = "recovery.solidcraft";
/// The autosave earlier releases kept (compatibility shim, docs/compatibility.md): recovered
/// when there is no current one.
const LEGACY_RECOVERY: &str = "recovery.solvecraft";
/// The autosave of unsaved work (in OPFS: `recovery.sc` and `recovery.json`), named with
/// `solidcraft_engine::io::DESIGN_EXT`.
const RECOVERY: &str = "recovery.sc";
/// The autosaves earlier releases kept, newest first, one per entry of
/// `solidcraft_engine::io::LEGACY_DESIGN_EXTS` (compatibility shim, docs/compatibility.md): the
/// newest is recovered when there is no current one.
const LEGACY_RECOVERY: [&str; 2] = ["recovery.solidcraft", "recovery.solvecraft"];
const RECOVERY_META: &str = "recovery.json";
/// What to do with a file brought in from the computer.
@@ -282,9 +284,10 @@ pub fn start(files: &Rc<Files>) {
}
let mut entries = Vec::new();
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
for (name, bytes, modified) in all {
if name == LEGACY_RECOVERY {
if !has_recovery {
if LEGACY_RECOVERY.contains(&name.as_str()) {
if legacy == Some(name.as_str()) {
vfs::insert(&format!("{OPFS}{RECOVERY}"), bytes);
}
continue;
@@ -424,7 +427,9 @@ pub fn autosave(files: &Rc<Files>, doc: Option<(&solidcraft_engine::doc::Documen
}
None => spawn_local(async move {
let _ = remove_file(RECOVERY).await;
let _ = remove_file(LEGACY_RECOVERY).await;
for legacy in LEGACY_RECOVERY {
let _ = remove_file(legacy).await;
}
let _ = remove_file(RECOVERY_META).await;
}),
}
@@ -513,9 +518,9 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
input.set_type("file");
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
let accept =
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
input.set_accept(&accept);
let mut extensions = solidcraft_engine::io::formats::open_extensions(true);
extensions.extend(["dxf", "svg"]);
input.set_accept(&disk::input_accept(&extensions, apple_touch()));
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
let _ = input.set_attribute("style", "display:none");
if let Some(body) = doc.body() {
@@ -546,7 +551,7 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
/// `None` when the dialog was closed or the file could not be read.
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
// Shown now, in the click: a file dialog needs the user's gesture.
let chosen = choose_file(&disk::accept_list(extensions));
let chosen = choose_file(&disk::input_accept(extensions, apple_touch()));
let limit = disk::upload_limit(extensions);
let files = files.clone();
async move {
@@ -573,6 +578,14 @@ pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::
}
}
/// The page runs on an iPhone or iPad, whose file inputs offer every file (`disk::input_accept`).
fn apple_touch() -> bool {
web_sys::window().is_some_and(|w| {
let nav = w.navigator();
disk::apple_touch(&nav.user_agent().unwrap_or_default(), nav.max_touch_points())
})
}
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
+9 -4
View File
@@ -72,10 +72,10 @@ 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 = "/linked/design.solidcraft";
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) {
if app.session.path.as_deref() == Some(path.as_str()) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
@@ -90,9 +90,14 @@ async fn fetch(value: &str) -> Reply {
let window = web_sys::window().ok_or("No browser window")?;
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
let url = design_url(value, &page_origin)?;
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design");
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
// `arm.sc` (or a legacy `arm.solidcraft`) is named `arm`.
let stem = match decoded.rsplit_once('.') {
Some((stem, ext)) if !stem.is_empty() && solidcraft_engine::io::is_design_ext(ext) => stem,
_ => decoded.as_str(),
};
let name: String = stem.chars().take(255).collect();
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
+1 -7
View File
@@ -8,15 +8,9 @@ publish = false
default-run = "solidcraft"
[features]
default = ["raytrace-cpu", "bundled-render-library", "smlib", "profile"]
default = ["raytrace-cpu", "bundled-render-library"]
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
# only kernel, so without it the app models nothing.
smlib = ["solidcraft-engine/smlib"]
# The profiler, compiled in so any build can say where its time goes (the Profile operations
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
profile = ["solidcraft-engine/profile"]
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
# (macOS file-open events), src/native_crash.rs (Windows exception filter), src/native_crash_macos.rs
+17 -7
View File
@@ -417,7 +417,6 @@ impl Lines {
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
_counted: Subscriber,
}
impl Subscription {
@@ -435,10 +434,13 @@ impl Subscription {
let stop = Arc::new(AtomicBool::new(false));
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
let (l, s) = (lines.clone(), stop.clone());
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
// 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()?;
let sent = send(&mut &lines.socket, reply);
drop(first);
Some((Subscription { lines, stop, _counted: counted }, sent))
Some((Subscription { lines, stop }, sent))
}
}
@@ -458,8 +460,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`.
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
/// 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) {
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() {
@@ -866,11 +870,17 @@ mod tests {
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
// Closed, it is no longer counted.
// Closed, it is no longer counted. Each publish asks the events thread for one more line,
// and its write to the closed connection is what retires the seat; that keeps the wait off
// the connection's own thread, which anything else this process is doing may hold up past
// the deadline (#7226).
drop((out, lines));
let mut revision = 11;
let deadline = Instant::now() + Duration::from_secs(10);
while WATCH.subscribed() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
WATCH.publish("d-1", revision);
revision += 1;
std::thread::sleep(Duration::from_millis(20));
}
assert!(!WATCH.subscribed());
}
+126 -20
View File
@@ -1,6 +1,6 @@
//! SolidCraft desktop app.
//!
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]`
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.sc | part.step]`
//!
//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and
//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md.
@@ -92,6 +92,8 @@ struct App {
native_menu: Option<native_menu::NativeMenu>,
/// A device's own app moving the view (Preferences › Navigation, off by default).
nav_bridge: nav_bridge::NavBridge,
/// Where the next file dialog opens: it follows the open design (#7756).
dialog_folder: DialogFolder,
}
impl eframe::App for App {
@@ -122,6 +124,7 @@ impl eframe::App for App {
// Automation runs (a control port was asked for) leave the user's AI apps alone.
if !self.take_focus_unwanted {
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
solidcraft_ui_egui::agent_chat::begin(&mut self.solve);
}
if let Some(dir) = self.starting.take() {
startup::started(&dir);
@@ -146,6 +149,7 @@ impl eframe::App for App {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
self.dialog_folder.design(self.solve.session.path.as_deref());
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
self.space_mouse.tick(&mut self.solve, ctx);
@@ -240,6 +244,60 @@ impl App {
}
}
/// Where the next file dialog opens (#7756): the folder of the file last chosen in one, or of the
/// design last opened or saved, whichever came last. Linux dialogs otherwise open in the home
/// folder every time; macOS and Windows remember a folder of their own, which this agrees with.
#[derive(Clone, Default)]
struct DialogFolder(std::sync::Arc<std::sync::Mutex<DialogFolderState>>);
#[derive(Default)]
struct DialogFolderState {
folder: Option<std::path::PathBuf>,
/// The open design's path when last looked at, to notice another design opened or saved.
design: Option<String>,
}
impl DialogFolder {
/// A file was chosen in a dialog: the next one opens in its folder.
fn chosen(&self, file: &std::path::Path) {
if let (Some(folder), Ok(mut state)) = (file.parent().filter(|f| !f.as_os_str().is_empty()), self.0.lock()) {
state.folder = Some(folder.to_path_buf());
}
}
/// The open design's path, every frame: a design opened or saved somewhere else moves the
/// next dialog to its folder; the same design again leaves a folder chosen since alone.
fn design(&self, path: Option<&str>) {
let Ok(mut state) = self.0.lock() else { return };
if state.design.as_deref() == path {
return;
}
state.design = path.map(str::to_string);
if let Some(folder) = path.and_then(|p| std::path::Path::new(p).parent()).filter(|f| !f.as_os_str().is_empty()) {
state.folder = Some(folder.to_path_buf());
}
}
/// A dialog's answer as a path, its folder remembered for the next dialog.
fn answer(&self, picked: Option<rfd::FileHandle>) -> Option<String> {
let picked = picked?;
self.chosen(picked.path());
Some(picked.path().to_string_lossy().into_owned())
}
fn folder(&self) -> Option<std::path::PathBuf> {
self.0.lock().ok().and_then(|state| state.folder.clone())
}
/// The dialog, opening in the remembered folder when there is one.
fn start(&self, dialog: rfd::AsyncFileDialog) -> rfd::AsyncFileDialog {
match self.folder() {
Some(folder) => dialog.set_directory(folder),
None => dialog,
}
}
}
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
/// case, plus FreeCAD's own mixed casing.
fn case_variants(exts: &[&str]) -> Vec<String> {
@@ -289,15 +347,16 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
raw.safe_area_insets = Some(insets);
}
fn services(cc: &eframe::CreationContext<'_>) -> Services {
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
let program_parent = parent.clone();
let file_parent = parent.clone();
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
Services {
pick_open: Some(Box::new(move |first: &[&str]| {
// The file type the command reads comes first, so the picker opens on it (#1801).
let mut dialog = open_parent.clone();
let mut dialog = open_folder.start(open_parent.clone());
if !first.is_empty() {
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
dialog = dialog.add_filter(label, &case_variants(first));
@@ -310,7 +369,8 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
let future = dialog.add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let folder = open_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
// One kind of file (the agent chat's attachments): only the types it names, as given and in
// lower and upper case (Linux dialogs match case; macOS allows just the listed types).
@@ -325,8 +385,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
}
let all: Vec<&str> = all.iter().map(String::as_str).collect();
// Any other file attaches by reference, so the list is not the only choice.
let future = file_parent.clone().add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let future = file_folder.start(file_parent.clone()).add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
let folder = file_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
pick_program: Some(Box::new(move || {
let dialog = program_parent.clone().set_title("Choose your slicer");
@@ -335,7 +396,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
pick_save: Some(Box::new(move |name: &str, exts: &[&str]| {
let dialog = parent.clone().set_file_name(name);
let dialog = save_folder.start(parent.clone()).set_file_name(name);
let dialog = if exts == [io::DESIGN_EXT, "json"] {
if name.ends_with(".json") {
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
@@ -346,7 +407,8 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
dialog.add_filter("File", exts)
};
let future = dialog.save_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let folder = save_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
@@ -553,26 +615,26 @@ fn graphics_path() -> Option<std::path::PathBuf> {
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
/// settings of earlier releases into the new folder. They kept `graphics.json` (and, on Linux
/// and macOS, the recovery folder) under the old product name, and eframe's `app.ron` under the
/// window's old app id. In the Flatpak sandbox, releases before 0.8 kept everything under the
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
/// window's old app id. In the Flatpak sandbox, earlier releases kept everything under an
/// old app id's `~/.var/app` folder; those copies come first, newest first. The old folders
/// are never changed.
fn adopt_legacy_settings() {
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(&current);
if let Err(e) = adopt_settings(&current, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
let old_flatpak = solidcraft_engine::io::legacy_flatpak_paths(&current);
if let Err(e) = adopt_settings(&current, &old_flatpak, &legacy, legacy_eframe.as_deref()) {
log::warn!("settings: could not copy the previous release's settings into {}: {e}", current.display());
}
}
fn adopt_settings(
current: &std::path::Path,
old_flatpak: Option<&std::path::Path>,
old_flatpak: &[std::path::PathBuf],
legacy: &std::path::Path,
legacy_eframe: Option<&std::path::Path>,
) -> std::io::Result<()> {
let fresh = !current.exists();
if let Some(old) = old_flatpak {
for old in old_flatpak {
solidcraft_engine::io::adopt_legacy_dir(current, old)?;
}
solidcraft_engine::io::adopt_legacy_dir(current, legacy)?;
@@ -679,8 +741,6 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
mark("main");
// SOLIDCRAFT_PROFILE=1: the profiler records from the start (diagnostics.profile reads it).
solidcraft_engine::timing::profile_from_env();
// 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() {
@@ -690,11 +750,14 @@ fn main() -> eframe::Result {
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
let mut files = Vec::new();
let mut sample = false;
// `--theme-file PATH`: follow that theme file for this run without saving it (#7573).
let mut theme_file: Option<String> = None;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
"--sample" => sample = true,
"--theme-file" => theme_file = args.next(),
"--safe-mode" => safe_mode = true,
"--version" | "-V" => {
println!("solidcraft {}", env!("CARGO_PKG_VERSION"));
@@ -714,7 +777,9 @@ fn main() -> eframe::Result {
return Ok(());
}
"--help" | "-h" => {
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]");
println!(
"usage: solidcraft [--control PORT] [--sample] [--theme-file THEME.json] [--crash-log] [--safe-mode] [design.sc | part.step]"
);
return Ok(());
}
_ => files.push(a),
@@ -842,7 +907,8 @@ fn main() -> eframe::Result {
solidcraft_ui_egui::fonts::setup_bundled_fonts(&cc.egui_ctx);
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
mark("fonts");
let mut app = SolveApp::new(Session::default(), services(cc));
let dialog_folder = DialogFolder::default();
let mut app = SolveApp::new(Session::default(), services(cc, &dialog_folder));
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
app.custom_titlebar = custom_titlebar;
#[cfg(target_os = "windows")]
@@ -865,6 +931,11 @@ fn main() -> eframe::Result {
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
if let Some(path) = &theme_file
&& let Err(e) = solidcraft_ui_egui::prefs::follow_theme_file_for_run(&mut app, path)
{
app.set_status(format!("--theme-file: {e}"), true);
}
mark("app state and prefs");
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
lost: rs.device_lost.clone(),
@@ -995,6 +1066,7 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
native_menu,
nav_bridge,
dialog_folder,
}))
}),
);
@@ -1173,6 +1245,40 @@ mod backend_tests {
}
}
#[cfg(test)]
mod dialog_folder_tests {
use super::DialogFolder;
use std::path::{Path, PathBuf};
/// #7756: a file dialog opens where the last one left off, not in the home folder.
#[test]
fn the_next_dialog_opens_in_the_folder_of_the_file_chosen_last() {
let folder = DialogFolder::default();
assert_eq!(folder.folder(), None, "before any choice the system decides");
folder.chosen(Path::new("/work/prints/bracket.stl"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/prints")));
// A bare file name has no folder to remember.
folder.chosen(Path::new("bracket.step"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/prints")));
}
/// Save As starts beside the open design (#7756), and a folder chosen since wins until another
/// design is opened or saved.
#[test]
fn the_open_design_moves_the_folder_only_when_it_changes() {
let folder = DialogFolder::default();
folder.design(Some("/work/lid/lid.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/lid")));
folder.chosen(Path::new("/exports/lid.3mf"));
folder.design(Some("/work/lid/lid.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/exports")), "the same design again leaves the export folder");
folder.design(None);
assert_eq!(folder.folder(), Some(PathBuf::from("/exports")), "a new, unsaved design has no folder of its own");
folder.design(Some("/work/base/base.solidcraft"));
assert_eq!(folder.folder(), Some(PathBuf::from("/work/base")));
}
}
#[cfg(test)]
mod viewport_tests {
#[cfg(target_os = "macos")]
@@ -1282,14 +1388,14 @@ mod msaa_tests {
std::fs::create_dir_all(&legacy_eframe).unwrap();
std::fs::write(legacy.join("graphics.json"), r#"{"msaa":8}"#).unwrap();
std::fs::write(legacy_eframe.join("app.ron"), "(prefs)").unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("graphics.json")).unwrap(), r#"{"msaa":8}"#);
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(prefs)");
assert!(legacy.join("graphics.json").is_file() && legacy_eframe.join("app.ron").is_file());
// Later launches leave the current settings alone.
std::fs::write(current.join("app.ron"), "(mine)").unwrap();
std::fs::remove_file(current.join("graphics.json")).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(mine)");
assert!(!current.join("graphics.json").exists());
let _ = std::fs::remove_dir_all(&d);
+7 -2
View File
@@ -146,8 +146,7 @@ impl NativeMenu {
}
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
action.enabled(app)
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
action.menu_enabled(app, ctx.egui_wants_keyboard_input())
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
@@ -157,6 +156,12 @@ impl NativeMenu {
item.set_enabled(enabled);
}
}
for (item, action) in &self.help_actions {
let enabled = solidcraft_ui_egui::help::available(app, action);
if item.is_enabled() != enabled {
item.set_enabled(enabled);
}
}
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
&& let Err(error) = self.update_recent(app)
{
+22 -5
View File
@@ -543,9 +543,16 @@ mod tests {
#[test]
fn probing_a_backend_alone_never_panics() {
// Whatever this machine has (often nothing): an answer, not a crash.
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
// Whatever this machine has (often nothing): an answer, not a crash. On Windows the probe
// goes the way the app's does, stopping at the first backend with a GPU: a machine with
// D3D12 never loads its OpenGL driver into the test process, and AMD's (`atio6axx.dll`)
// aborted the process as it exited (#1156).
if cfg!(target_os = "windows") {
let _ = choose_backends(true, false, false, None, has_hardware_adapter);
} else {
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
}
}
}
@@ -558,8 +565,18 @@ mod tests {
#[test]
fn the_software_adapter_is_preferred_and_its_absence_is_an_error_not_a_panic() {
assert!(pick_cpu_adapter(&[], None).unwrap_err().contains("none"));
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
let adapters = pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all()));
// The adapters the software renderer chooses among: it starts D3D12 alone on Windows
// (WARP), not every backend, so the test doesn't load the OpenGL driver either (#1156).
let options = crate::wgpu_options(true, None);
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &options.wgpu_setup else { panic!("the software renderer makes its own device") };
let backends = setup.instance_descriptor.backends;
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();
desc.backends = backends;
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
if adapters.is_empty() {
return;
}
+1 -1
View File
@@ -76,7 +76,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `io.tangencylabs.solidcraft` |
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/`, uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
+93
View File
@@ -0,0 +1,93 @@
# Render workspace icon drafts
All ids below are **Open** for owner review, except `render_scene` and `render_image`,
which are existing accepted icons used only as references. No commands, toolbar
entries, browser selectors or runtime request manifests are changed by this draft set.
`render_remove_override` is the one remaining alias (of `render_from_material`);
`render_execute`, `render_environment` and `render_fit_product` now have their own
masters in `design/svg/model/`. Accepted source masters stay unchanged.
Review with `python3 -B assets/icons/ui/design/tools/icon_status.py --serve`,
search `render_`, and select Open. See [STYLE_GUIDE.md](STYLE_GUIDE.md) for
acceptance; drafts do not imply implemented commands.
The finished references in each row were read and previewed in both themes
before authoring. Draft contact sheets (96, 24 and 16 px, both themes) are local
review evidence, not shipped assets. The icons are grouped in five families, each
with one shared design rule.
## Lights
Rule: a slate source glyph with light passive fill, 2.55-unit outlines and a dotted
accent stem (three dots, 4.25 diameter, 7 units apart) showing the direction.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Light area | `render_light_area` | `box`, `camera` | Slanted flat panel on the canonical axes above the dotted stem. |
| Light spot | `render_light_spot` | `camera`, `material` | Cone with a curved rim above the dotted stem. |
| Light point | `render_light_point` | `camera`, `material` | Round source disc with a centre dot above the dotted stem. |
| Sun | `render_sun` | `sphere`, `camera` | Source disc of the same size with eight short radial rays instead of a stem. |
| Light by highlight | `render_light_by_highlight` | `sphere`, `material` | Shaded sphere with a ringed highlight target; the dotted stem arrives from above. |
| Solo light | `render_solo_light` | `fit`, `camera` | Point source and stem inside four corner focus brackets. |
## Looks
Rule: the lit look sphere (top-left highlight, bottom-right shade) plus one cue that
says what happens to it; receiving bodies stay canonical grey or cube shapes.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Appearance | `render_appearance` | `material`, `box` | Shaded look sphere on the top corner of an accent-shaded cube: a look assigned to a body. |
| Paint | `render_paint` | `material`, `paste` | Flat brush with an accent tip, light ferrule and slate handle. |
| Pick up | `render_pick_up` | `material`, `copy` | Eyedropper with a grey bulb, slate crossbar and accent sampling tip. |
| Replace all | `render_replace_all` | `copy`, `box` | Grey sample sphere with an arrow to an accent sphere, beside a second accent sphere. |
| Copy look | `render_copy_look` | `material`, `copy` | Look sphere in a square frame, offset from a second frame outline. |
| Remove override | `render_remove_override` | `appearance`, `box` | Alias of `render_from_material`: the custom look is crossed off. |
| Same look | `render_same_look` | `select`, `box` | Look sphere and three cubes; the two matching cubes are ringed. |
| From material | `render_from_material` | `material`, `extrude` | Look sphere struck through above a plain grey slab, so the material shows. |
## Studio
Rule: the standard grey cube stands in the set-up it is lit or placed in.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Library | `render_library` | `library`, `copy` | Row of three look spheres, the middle one accent, on a shelf bar. |
| Lighting presets | `render_lighting_presets` | `new`, `render_scene` | Two stacked cards, the front one holding a light bulb. |
| Environment | `render_environment` | `environment_map`, `render_scene` | Dome with a grey cube inside. |
| Ground | `render_ground` | `box`, `render_scene` | Grey cube on a slab with an accent contact patch. |
| Backdrop | `render_backdrop` | `render_scene`, `render_image` | Grey cube in front of a tall upright accent panel. |
## Camera and shots
Rule: one chunky camera body with a lower-right round badge for shot actions; the
other camera icons are simple frames.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Shot | `render_shot` | `camera`, `new` | Camera body with a plus badge: save a shot. |
| Update shot | `render_update_shot` | `camera`, `undo` | Camera body with a refresh badge. |
| Render all shots | `render_all_shots` | `copy`, `render_image` | Stacked camera bodies with a play badge. |
| Shots strip | `render_shots_strip` | `camera`, `copy` | Strip of three small frames, the middle one in accent. |
| Fit product | `render_fit_product` | `fit`, `camera` | Grey cube inside four corner brackets. |
| Lens / field of view | `render_lens` | `camera`, `fit` | Concentric lens rings around an accent centre. |
| Exposure | `render_exposure` | `material`, `environment_map` | Disc split light and accent, with plus and minus marks. |
| Aspect frame | `render_aspect_frame` | `render_image`, `fit` | Wide rounded frame with two opposite corner brackets. |
| Frame guides | `render_frame_guides` | `render_image`, `fit` | Rounded frame with a rule-of-thirds grid and four accent nodes. |
## Output and result
Rule: one picture base (frame with landscape) with a lower-right round badge; the
size, background and quality icons draw their own simple subject.
| Meaning | Id | Finished references | Drawing |
|---|---|---|---|
| Render image | `render_execute` | `render_image`, `camera` | Picture with a play badge: create a rendered image. |
| Render output size | `render_output_size` | `render_image`, `fit` | Picture frame with a diagonal double-headed dimension arrow. |
| Transparent background | `render_transparent_background` | `material`, `render_image` | Grey cube on a checker tile. |
| Quality | `render_quality` | `list`, `library` | Three rising bars, the tallest in accent. |
| Check render | `render_check` | `render_image`, `new` | Picture with a check badge. |
| Cancel render | `render_cancel` | `render_image`, `delete` | Picture with a square stop badge. |
| Export image | `render_export_image` | `render_image`, `copy` | Picture with a large outward diagonal arrow at the upper right. |
| Compare | `render_compare` | `render_image`, `material` | Two grey pictures split by a vertical slider with opposed arrows. |
| Results | `render_results` | `render_image`, `copy` | Stack of two pictures, the front one showing a landscape. |
+7
View File
@@ -27,6 +27,9 @@ error = { master = "status_error", accent = "surface" }
warning = { master = "status_warning", accent = "mesh" }
ground = "c_fix"
# Visual style: Shaded with edges is the generic style icon.
style_shaded_edges = "style"
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
sketch_break = { master = "break", accent = "sketch" }
sketch_cancel = { master = "close", accent = "sketch" }
@@ -45,3 +48,7 @@ sketch_chamfer_equal = { master = "sketch_chamfer", accent = "sketch" }
sketch_chamfer_two = { master = "sketch_chamfer_two_distances", accent = "sketch" }
offset_one_side = { master = "offset", accent = "sketch" }
bom = "list"
# Render workspace drafts: shared meanings; owner review remains Open.
# Removing a look override returns the body to its material look.
render_remove_override = "render_from_material"
@@ -1,6 +1,8 @@
accessibility false
activate false
agent_chat false
align false
ambient_occlusion false
appearance false
arc3 false
arc_center false
@@ -83,6 +85,8 @@ decal false
delete false
delete_face false
dimension false
display false
dots false
draft false
draft_analysis false
drive false
@@ -110,7 +114,9 @@ folder false
frame_member false
gear false
graph false
grid false
ground false
ground_shadow false
help false
hem false
hole false
@@ -210,6 +216,11 @@ section false
select false
send false
settings false
settings_keyboard false
settings_monitor false
settings_mouse false
settings_palette false
settings_plug false
sheet_rules false
shell false
shield false
@@ -247,6 +258,11 @@ star false
status_error false
status_recovery false
style false
style_hidden_edges false
style_shaded false
style_shaded_edges false
style_translucent false
style_wireframe false
suppress false
surface false
surface_blend false
+62
View File
@@ -6,6 +6,7 @@
"agent_chat": "4a9ae0e7e2c360b4858b3175674c93d096705c92318fecd481e849f3ae650bf4",
"agent_link": "2f20d89a0e3e9b28744910dddf7f282a1099ccd91aa160c9841c5b1d9ea9ed0f",
"align": "05615db7dd9e4be56ae898e59a68c548b8a8447794442a9a394940651204bc4f",
"ambient_occlusion": "973f8ef2326aeaf0face576e5a2c79c7ef3d13638b2c51a5aad199e4ef7edb02",
"appearance": "b3162edd92291a5ef402e8499cd52be05beb2fa7eda641163c3f6e98325de2bf",
"arc3": "fdca3d501fd557ac989376a12e9c4fa8231fde2b455c20724d1079a466153e9c",
"arc_center": "3a5fdeb1d64fc6741f944c4435870b05fccab62293e270da692c6681155cbc51",
@@ -88,6 +89,7 @@
"dimension": "a926b7c64bc8ca99d8d6dcbb81b95235bd3fc34fbed049aca8b01b4d1147c15d",
"disclosure_closed": "49a350c6f2efae78de0be96c524ebee1fd1291ea449a84224b88d37164a4e807",
"disclosure_open": "d13ce3f97e3e362a9904fa2bf3412f3f00e4136bc23538b92d41483961d280de",
"display": "58b45e5e2230eaf2c80f74b3be2e02a23714c90d6fb03b59ed093a1949993163",
"dots": "600f7ff209d93145fc938bc4f6a1e34473e6b9ee22a5b4718f133bc7da5ee58d",
"draft": "9c0fc361cacd0ad95ccd54794f4825dfc9901a83a3de752a588faec365d0ee09",
"draft_analysis": "eb39c0f90e9cf226da6307365a76015e900dd692dc3f0fdc8f02fe717226261f",
@@ -101,6 +103,7 @@
"error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"explode": "2aecb05d5f05aff1ab629066cb9bea8718ea213ac70948ef71a3ced3321f4cb2",
"export": "c8c2abf007e43e896479d78798e3976ba9eda809507c820bb38538f77db696ac",
"export_tray": "f18606e1f63726c57a0787dced4fb8a221b4a4fc18b88b0bbdee743e546cb489",
"extend": "28c63df352c77d02feffe44cf09fb0fc1d48f7905aaabbfa6dba601dbb70f8e8",
"extrude": "9cc1538971c5d25467aaed23f739899d9d102fd07f86472ac3197c54ca2e1144",
"eye": "2449167c8410826dc1106b687c12a309d5bea8f7817b897943e80f182a163c2d",
@@ -116,12 +119,15 @@
"folder": "c19f5230df45a1bda1e92e89de38bba21c91e6eca0517eb15847a7712012d1e5",
"gear": "25a1ca48a587850da1104a46005a330873ed196246e3d04e12cf61f26ad2b0d5",
"graph": "7156caedd2dbfa2ae53da18b7a2204b8db66c45126859d36caf049a3fd4335a4",
"grid": "084b5040d57e9d5183b3db1fad0d17a13ab5bae64676eef703cc06622f9c7438",
"ground": "23e2471a9d6872e83028e0e219e330cef1930a4d89456a0fefa1bda8a7e3eb81",
"ground_shadow": "a96809ed87f5047c2d7f1274908ed7b65b180613322ac9c10eb0f9d6296c1b66",
"help": "8ce4060577483dbdb510be147d2827de50ac20029354008fca55abc0c7edf05d",
"hem": "f9b1aafbedcf3dc90c26be308814638e0095fbe31b0647092507343fa60af011",
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
"home": "ab5f1c41d377687e4c5a808dfdccf2757b33dd43f934d44df43d6e9118176853",
"import": "e343540eb49fdb1e53c4d54b9e60ae513b3103df6c9133048bed878e58067455",
"import_tray": "c6aec9556dc8ecb6c953d6bf1f384f220c68167a92e444950c24ded81f66a0b6",
"include": "f5b80ad3e06e3b863fcfbe415ec912e4e09653dd519f9ac207819339fe34730f",
"infinite": "53fb781131078d2f8e71be23aa610d9f9d9c8bb79ba5f97fa397475e9ff8c3bd",
"inspect": "d2b4644b2d5f1aa50cb576b9265e275fa91201e296c2bc79865f810067d538e9",
@@ -206,8 +212,45 @@
"redo": "28e925493fa9e5f52e2c8e3beeb288af13fdb4f85f3cd92270096a10561e7a1a",
"refold": "1fa6a8604af7d04c01bc31d0e5875eb2cc91f5fa6bfc5491dd6680d3299eb26f",
"rename": "6887c5467101dca1d213154020d3b03be9b2704ac310de9d0a8ebde62c013bf5",
"render_all_shots": "9c04f49624cc2685472041cb3f3e95da2c5f32deeebb9723f4a8e3c369680a5b",
"render_appearance": "53287b30915a266d8a3858d5bef24b93ed11f8d612c20c445d3032f400a925cc",
"render_aspect_frame": "5d74b263eeb03eb0494b3774817135b8f119caff205a8b31c480d7dd0e5a8554",
"render_backdrop": "d27f7d6da7a31c5944dd0ff67c21838690f000f2d87b0bf5274bef0744de34ce",
"render_cancel": "2f1d8b3039007ccfc224390758e05d6202fa2eabf63724c744217dcab5d58e5e",
"render_check": "65ccf0407d80937c6931cbf87b6d0e628cfb62ca3e841483d02dd9de19c3717c",
"render_compare": "1b2d3f3920782490b96b4d14a7cc9506eff98ff2efae00f86d911fabcabc7cb0",
"render_copy_look": "25e9138a839a7e6b8225b48cd2f36daba42a928eff541fe616f3df39a1bc82b7",
"render_environment": "50cb18380d9310f6e04084c24ac85529599966b1d90c6bef514f4e63f9b30854",
"render_execute": "e4d9a66f4569e00c4b4b2ac05a80a9f3eba4e59802c62d3af5bb951ba9153cbf",
"render_export_image": "2456f55fa3b50ab84ae871963d4a6b6e92f486cf6ed3039f3a2f07f11192a655",
"render_exposure": "60eb5fa040aefe872ab2de66099493513a494ab758a23d443753d033eaf3a05d",
"render_fit_product": "8e58b293a39a06fca4cda0543adc2afef3d0041fe34b0bc0c800afb2d5635123",
"render_frame_guides": "cb1df01051de42047e38de9dd2a96fcb5b67bd5979cb5756412890400b69e5bc",
"render_from_material": "c928daa803a28f90ce24096731b8de711c6cd3215259ac9cd761b6be2e1b9618",
"render_ground": "2b900b433870cbc4bd0282e54c604ec696aa28354c753a13b09806f5e87d99c3",
"render_image": "e3dd622591de4fdc211f9bd3c6dcb54b01a308e8bcebd02bb80dd6b3982ce2a5",
"render_lens": "79bb615d46a05ea5ffc9e2e905cc25f266b3b7e761c160f7d25bbdb3dea0a430",
"render_library": "828af74034a62616c774ab0cbe70b7cf9fd42778b574febfb1812f8aa17fd49b",
"render_light_area": "b1f3ee596ee1d35f6e8444b54e94f36a34c2129697c88e5286dbf38565f50816",
"render_light_by_highlight": "5b37c8c6aa27ac2002dd8c8094e415c64e9eb315a55c4f146e328314e8f639a2",
"render_light_point": "a602cb5fa26f04f1bb9be7c14e5a9a9ce52b37550dfd3211a55e915f941f5803",
"render_light_spot": "a9413de906266bec0165b4cb06ecac8e36dce0b6ddec4bc4cc6ff4f152601483",
"render_lighting_presets": "e0a08433f548a9dd139296107b04b1d4c57ff04303c9bee32d896e2ea06a8d07",
"render_output_size": "f08fe354e4d5a6dd21af29cb077ceed96c413c68f48d9f72548ae1614337f88b",
"render_paint": "e3d3202205a5a90906348cae519340d38f475e42039a7f60c835cf7a21081da1",
"render_pick_up": "df67d10f333a3c9e367897d599a4be510f10ed36c24763c26817f22d24ff35b7",
"render_quality": "28fe44cdd2b2f58b91d91dcf20a6dfbd22cf3db6008e1db179717523ecc4889e",
"render_remove_override": "c928daa803a28f90ce24096731b8de711c6cd3215259ac9cd761b6be2e1b9618",
"render_replace_all": "3bbf830619a91be186b9dc91726b8e47c293556d2ac8c93d87c71014c9f3abcd",
"render_results": "94e78898d88eef415e56ca5ec1e056089a2a5aa709abc69e5b8103d6096a019c",
"render_same_look": "2b5c69c523a1c949c22638c378803e77123095fddd36cfa58e94b2fd91ca61d7",
"render_scene": "1c4d7350f6f17ad637a7d633cb65a64c298af20321b1dbdac01b51d47a0e2822",
"render_shot": "4428080faa94f276c8a1d20eb38d04835ee5ae9d6337402371a5edf09f97ddc5",
"render_shots_strip": "1dcf5cc8e704691985284e0ca06e03b22540ea9d4cd23f4fb8675801ae7e69ec",
"render_solo_light": "abb9f8993be108a79d491bfaca32cde59d07a69db3ead58195e8ca0d8e8d909b",
"render_sun": "d86b51f3d4d83df4e932a22bb20d567513529a2eaaa42b93f744412eed2be901",
"render_transparent_background": "71dab4763516053734d4eec964b0899716d617597fe0d09e3bfa0cc36224dd7a",
"render_update_shot": "22d82ad332f8325395596c4bec4c1ec9bbb72d9fc4cd62ad658a93d7f5f803b9",
"reorder": "c45755c8f23575415397bffb316eed9f2d8c014f451ab6f8c3123aca1a99b675",
"replace_face": "fcee556c684a1138a30a2574ea565c98ff29f9368c2e861d0fb861155a57d28b",
"rest": "eab64961b5340bdbbd92affe0c9b8477dc297a3b49e05e39d5fa71c0f2fda66c",
@@ -224,10 +267,23 @@
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
"select_bodies": "8e6e61e083e6097535da3b98d41b607b40fc81b72bbd8c7fb903618c5b35c568",
"select_components": "3e6cc847b334e92ce7ec5e5f35dfa929572e0974239790d76563ca4e20b26789",
"select_faces_edges": "7b99b5370b1647bf8cea57df7a1053caab641c2f4a94c4d797b9679e6f38d6f1",
"select_features": "b050bc19ce86d135cbe50998be681555110e92e179c4c7b101170c6b62c19943",
"select_parts": "569adc5595a64187411d8ffeea69eab3b76ddfa33aeead6fd728efdd993bcccc",
"select_sketch": "b0605b34a14d4173f7ed80d877109ec0b891a1b395bcab5743a603ed2eae240a",
"send": "f83b6cdab792f9567a81d46d7828ed20795513c57a2c7d00fbbe6bd14d2f5848",
"settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a",
"settings_keyboard": "4968c70788094365af9c6e2b1c15b2aac45cf21212a14b8a256daf1ffe17f41a",
"settings_monitor": "6fcea26e2fc1673c76c6e7fadac4a91812ed43250cfb363c4f40c0cacfbd7aa5",
"settings_mouse": "47754752ea6fb37ec9686c0460740935a4db30fb267f1136a9246aec9a17b3f1",
"settings_palette": "a060015a750ff4367c65c04b83bf43720e13a38d8dee35330e1d8880f89eaa22",
"settings_plug": "79c00b84b2d1d28e3bf6096639c75476b7e1ee0a326c34ad76a79ad866c0102e",
"sheet": "774e30923ef6f37dd9c0b2bc3e45705e83b3a76700bf071cd41155a365d0daa4",
"sheet_rules": "5d3a2f37d3b4a4262b3ddbdae53d61f29e0f8e284dd54006da25a7acebfa1114",
"shell": "a1959ea0d0441a479bf654b6e549e2e44a0bfdc9cda4eb46fe1fbfd884de7e38",
"shield": "5e2d2d157309e0daa72f7c53073dfdb0a474def972a041cd172567f1d1e485ec",
"silhouette": "2a848aed2f247b1c0439f830a34a513c93bc5493c40044c821d493d863ad996e",
"sketch": "b52e5862b1709a9a7e3ca00d7436d126fbfc01b38080006fa4e17dc581dfa103",
"sketch_break": "67a5fab332616e6d6a9aeae3bedc04ee0768179143ba34b795bdd98cea04bb24",
@@ -261,7 +317,13 @@
"status_error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"status_warning": "1ac4f0ccfac2c01f6f66cb42188f9ae053c4aef9c5bbd249f1fb09611eb4d825",
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_hidden_edges": "a707dfc0bdcabe9144865f569abfcb478e493143cbd4331efc1404cf3082f6d2",
"style_shaded": "6e5f8f9a9030ab3717f8b2192bbfd3f6b515ad398acf6cb41bc24f1298bc73b4",
"style_shaded_edges": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_translucent": "c46b0cd485cfb9023514908357048b5cba9017a0c4976239da1e98f50e4ab5f9",
"style_wireframe": "af7e575d378b5b6850bee9d3ca384efab95c8d4c876c5e3524f3ea771c4380a3",
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
"surface_blend": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc",
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ambient occlusion</title>
<g opacity="0.22"><path d="M13 51L51 51A38 38 0 0 0 13 13Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.38"><path d="M13 51L40 51A27 27 0 0 0 13 24Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.62"><path d="M13 51L29 51A16 16 0 0 0 13 35Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M13 10L13 51L54 51" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 758 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Display</title>
<path d="M10 22L54 22M10 42L54 42" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="24" cy="22" r="10.5" fill="#ffffff"/>
<circle cx="42" cy="42" r="10.5" fill="#ffffff"/>
<circle cx="24" cy="22" r="6.4" fill="currentColor"/>
<circle cx="42" cy="42" r="6.4" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Grid</title>
<path d="M12 12L52 12L52 52L12 52Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25.3 12L25.3 52M38.7 12L38.7 52M12 25.3L52 25.3M12 38.7L52 38.7" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ground shadow</title>
<path d="M32 4.8 L48.1 13.9 L48.1 32.1 L32 41.2 L15.9 32.1 L15.9 13.9Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.34 31.35A25 11 0 1 0 54.66 31.35" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Bodies</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Components</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25ZM41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.46 18.35L22.91 26.75L22.91 43.55" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Faces and edges</title>
<path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L9 19M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Features</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 19L32 32M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M42 19C42 22.2 37.52 24.8 32 24.8C26.48 24.8 22 22.2 22 19C22 15.8 26.48 13.2 32 13.2C37.52 13.2 42 15.8 42 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 632 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Parts</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25Z" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 585 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sketch features</title>
<path d="M10 46C18 14 46 14 54 46" fill="none" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="10" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="54" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="32" cy="22.5" r="8.6" fill="#ffffff"/>
<circle cx="10" cy="46" r="5.8" fill="currentColor"/>
<circle cx="54" cy="46" r="5.8" fill="currentColor"/>
<circle cx="32" cy="22.5" r="5.8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded with hidden edges</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L15.44 22.64M32 32L48.56 22.64M32 32L32 50.72" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 13.28M32 32L48.56 41.36M32 32L15.44 41.36" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="4.5 4"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded</title>
<g opacity="0.5"><path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.8"><path d="M9 19 L32 32 L32 58 L9 45Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 32 L55 19 L55 45 L32 58Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Translucent</title>
<defs>
<clipPath id="tr-clip"><path d="M32 8.6 L52.7 20.3 L52.7 43.7 L32 55.4 L11.3 43.7 L11.3 20.3Z"/></clipPath>
</defs>
<g clip-path="url(#tr-clip)"><path d="M-71 72L9 -8M-60.5 72L19.5 -8M-50 72L30 -8M-39.5 72L40.5 -8M-29 72L51 -8M-18.5 72L61.5 -8M-8 72L72 -8M2.5 72L82.5 -8M13 72L93 -8M23.5 72L103.5 -8M34 72L114 -8M44.5 72L124.5 -8M55 72L135 -8" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.76 20.56M32 32L52.24 20.56M32 32L32 54.88" fill="none" stroke="#ffffff" stroke-width="6.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.3 20.3M32 32L52.7 20.3M32 32L32 55.4" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Wireframe</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 6M32 32L55 19M32 32L55 45M32 32L32 58M32 32L9 45M32 32L9 19" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render all shots — every saved camera view at once</title>
<path d="M27.1 12L29.26 12L31.42 6.96Q31.85 5.52 33.22 5.52L36.82 5.52Q38.19 5.52 38.62 6.96L40.78 12L51.58 12Q55.18 12 55.18 15.6L55.18 31.44Q55.18 35.04 51.58 35.04L27.1 35.04Q23.5 35.04 23.5 31.44L23.5 15.6Q23.5 12 27.1 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M16.6 21L18.76 21L20.92 15.96Q21.35 14.52 22.72 14.52L26.32 14.52Q27.69 14.52 28.12 15.96L30.28 21L41.08 21Q44.68 21 44.68 24.6L44.68 40.44Q44.68 44.04 41.08 44.04L16.6 44.04Q13 44.04 13 40.44L13 24.6Q13 21 16.6 21Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6.1 30L8.26 30L10.42 24.96Q10.85 23.52 12.22 23.52L15.82 23.52Q17.19 23.52 17.62 24.96L19.78 30L30.58 30Q34.18 30 34.18 33.6L34.18 49.44Q34.18 53.04 30.58 53.04L6.1 53.04Q2.5 53.04 2.5 49.44L2.5 33.6Q2.5 30 6.1 30Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18.34" cy="41.52" r="7.56" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M43.9 41.7L53.3 47.5L43.9 53.3Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Appearance — a body wearing the look sphere</title>
<path d="M17.33 24.8L38.98 37.3L38.98 62.3L17.33 49.8Z" fill="#328bd3"/>
<path d="M60.63 24.8L38.98 37.3L38.98 62.3L60.63 49.8Z" fill="#236ead"/>
<path d="M38.98 12.3L60.63 24.8L38.98 37.3L17.33 24.8Z" fill="#b9ddf8"/>
<path d="M38.98 12.3L60.63 24.8L60.63 49.8L38.98 62.3L17.33 49.8L17.33 24.8Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17.33 24.8L38.98 37.3L60.63 24.8" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38.98 37.3L38.98 62.3" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18.98" cy="17.3" r="16.88" fill="#ffffff"/>
<circle cx="18.98" cy="17.3" r="14" fill="#328bd3"/>
<path d="M32.14 12.51A14 14 0 0 1 14.19 30.46A18.31 18.31 0 0 0 32.14 12.51Z" fill="#236ead"/>
<circle cx="14.22" cy="12.54" r="3.78" fill="#b9ddf8"/>
<circle cx="18.98" cy="17.3" r="14" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Aspect frame — the shape of the picture</title>
<path d="M13 17L51 17A4 4 0 0 1 55 21L55 43A4 4 0 0 1 51 47L13 47A4 4 0 0 1 9 43L9 21A4 4 0 0 1 13 17Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M4.5 23.5L4.5 12.5L15.5 12.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M59.5 40.5L59.5 51.5L48.5 51.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Backdrop — the coloured wall behind the product</title>
<path d="M30.69 4L54.25 17.6L54.25 41.6L30.69 28Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 928 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Cancel render — stop the running render</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#8e9faf"/>
</g>
<circle cx="36.2" cy="22.8" r="4.2" fill="#8e9faf"/>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M44.1 42.9L50.9 42.9A1.2 1.2 0 0 1 52.1 44.1L52.1 50.9A1.2 1.2 0 0 1 50.9 52.1L44.1 52.1A1.2 1.2 0 0 1 42.9 50.9L42.9 44.1A1.2 1.2 0 0 1 44.1 42.9Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Check render — verify the request is ready, without rendering</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#8e9faf"/>
</g>
<circle cx="36.2" cy="22.8" r="4.2" fill="#8e9faf"/>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M41.9 47.7L45.9 51.9L53.3 43.1" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Compare — live preview and render side by side</title>
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="cmpL"><path d="M4 12L32 12L32 52L4 52Z" fill="none"/></clipPath></defs>
<g clip-path="url(#cmpL)">
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="#b8c3cf"/>
</g>
<defs><clipPath id="cmp"><path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#cmp)">
<path d="M4 44L20.8 28.8L36.48 41.6L45.44 35.2L60 46.4L60 52L4 52Z" fill="#8e9faf"/>
</g>
<path d="M9 12L55 12A5 5 0 0 1 60 17L60 47A5 5 0 0 1 55 52L9 52A5 5 0 0 1 4 47L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 5L32 59" fill="none" stroke="#ffffff" stroke-width="8.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="14.5" fill="#ffffff"/>
<path d="M32 5L32 59" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="12.8" fill="#236ead"/>
<path d="M23.4 32L29.8 25.6L29.8 38.4Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M40.6 32L34.2 25.6L34.2 38.4Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Copy Look — the look in front of a copy outline</title>
<path d="M8 40L8 12.5A4.5 4.5 0 0 1 12.5 8L40 8" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M24 19L51 19A5 5 0 0 1 56 24L56 51A5 5 0 0 1 51 56L24 56A5 5 0 0 1 19 51L19 24A5 5 0 0 1 24 19Z" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="37.5" cy="37.5" r="12" fill="#328bd3"/>
<path d="M48.78 33.4A12 12 0 0 1 33.4 48.78A15.69 15.69 0 0 0 48.78 33.4Z" fill="#236ead"/>
<circle cx="33.42" cy="33.42" r="3.24" fill="#b9ddf8"/>
<circle cx="37.5" cy="37.5" r="12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 841 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Environment — a sky dome lighting the product</title>
<path d="M4.04 42A25.96 25.96 0 0 1 55.96 42A25.96 14.99 0 0 1 4.04 42Z" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 948 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render image — start rendering the picture</title>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 40.8L17.8 27.12L30.68 38.64L38.04 32.88L50 42.96L50 48L4 48Z" fill="#328bd3"/>
</g>
<path d="M9 12L45 12A5 5 0 0 1 50 17L50 43A5 5 0 0 1 45 48L9 48A5 5 0 0 1 4 43L4 17A5 5 0 0 1 9 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47.5" cy="47.5" r="14" fill="#ffffff"/>
<circle cx="47.5" cy="47.5" r="12" fill="#236ead"/>
<path d="M43.9 41.7L53.3 47.5L43.9 53.3Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 993 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Export image — save the finished picture as a PNG file</title>
<path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="#e2e8ef"/>
<defs><clipPath id="pic1"><path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="none"/></clipPath></defs>
<g clip-path="url(#pic1)">
<path d="M4 46.8L17.2 33.12L29.52 44.64L36.56 38.88L48 48.96L48 54L4 54Z" fill="#8e9faf"/>
</g>
<path d="M9 18L43 18A5 5 0 0 1 48 23L48 49A5 5 0 0 1 43 54L9 54A5 5 0 0 1 4 49L4 23A5 5 0 0 1 9 18Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28 36L47.77 17.55" fill="none" stroke="#ffffff" stroke-width="11.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58 8L53.52 25.18L40.55 11.29Z" fill="#ffffff" stroke="#ffffff" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28 36L47.77 17.55" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58 8L53.52 25.18L40.55 11.29Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Exposure — brighten or darken the picture</title>
<circle cx="32" cy="32" r="25" fill="#e2e8ef"/>
<path d="M32 7A25 25 0 0 1 32 57Z" fill="#236ead"/>
<circle cx="32" cy="32" r="25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 32L27 32M20.5 25.5L20.5 38.5" fill="none" stroke="#34465b" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37 32L50 32" fill="none" stroke="#ffffff" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Fit product — frame the whole product in the view</title>
<path d="M15.98 22.75L32 32L32 50.5L15.98 41.25Z" fill="#b8c3cf"/>
<path d="M48.02 22.75L32 32L32 50.5L48.02 41.25Z" fill="#8e9faf"/>
<path d="M32 13.5L48.02 22.75L32 32L15.98 22.75Z" fill="#e2e8ef"/>
<path d="M32 13.5L48.02 22.75L48.02 41.25L32 50.5L15.98 41.25L15.98 22.75Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15.98 22.75L32 32L48.02 22.75" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 50.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 18.5L5.5 5.5L18.5 5.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58.5 18.5L58.5 5.5L45.5 5.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M58.5 45.5L58.5 58.5L45.5 58.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.5 45.5L5.5 58.5L18.5 58.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Frame guides — rule of thirds over the picture</title>
<path d="M10 14L54 14A4 4 0 0 1 58 18L58 46A4 4 0 0 1 54 50L10 50A4 4 0 0 1 6 46L6 18A4 4 0 0 1 10 14Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23.33 15.3L23.33 48.7M7.3 26L56.7 26M40.67 15.3L40.67 48.7M7.3 38L56.7 38" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="23.33" cy="26" r="3.6" fill="#236ead"/>
<circle cx="23.33" cy="38" r="3.6" fill="#236ead"/>
<circle cx="40.67" cy="26" r="3.6" fill="#236ead"/>
<circle cx="40.67" cy="38" r="3.6" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>From Material — custom look crossed off, material shows</title>
<path d="M6 38L32 53.01L32 57.01L6 42Z" fill="#b8c3cf"/>
<path d="M58 38L32 53.01L32 57.01L58 42Z" fill="#8e9faf"/>
<path d="M32 22.99L58 38L32 53.01L6 38Z" fill="#e2e8ef"/>
<path d="M32 22.99L58 38L58 42L32 57.01L6 42L6 38Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 38L32 53.01L58 38" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 53.01L32 57.01" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="22" r="15" fill="#328bd3"/>
<path d="M46.1 16.87A15 15 0 0 1 26.87 36.1A19.62 19.62 0 0 0 46.1 16.87Z" fill="#236ead"/>
<circle cx="26.9" cy="16.9" r="4.05" fill="#b9ddf8"/>
<circle cx="32" cy="22" r="15" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 11L43 33M43 11L21 33" fill="none" stroke="#ffffff" stroke-width="6.65" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 11L43 33M43 11L21 33" fill="none" stroke="currentColor" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ground — the floor the product stands on, with its contact shadow</title>
<path d="M5.06 42L30 56.4L30 58.8L5.06 44.4Z" fill="#328bd3"/>
<path d="M54.94 42L30 56.4L30 58.8L54.94 44.4Z" fill="#236ead"/>
<path d="M30 27.6L54.94 42L30 56.4L5.06 42Z" fill="#b9ddf8"/>
<path d="M30 27.6L54.94 42L54.94 44.4L30 58.8L5.06 44.4L5.06 42Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5.06 42L30 56.4L54.94 42" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 56.4L30 58.8" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L45.93 44.8L34.85 51.2L18.91 42Z" fill="#328bd3"/>
<path d="M18.91 29.2L30 35.6L30 48.4L18.91 42Z" fill="#b8c3cf"/>
<path d="M41.09 29.2L30 35.6L30 48.4L41.09 42Z" fill="#8e9faf"/>
<path d="M30 22.8L41.09 29.2L30 35.6L18.91 29.2Z" fill="#e2e8ef"/>
<path d="M30 22.8L41.09 29.2L41.09 42L30 48.4L18.91 42L18.91 29.2Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.91 29.2L30 35.6L41.09 29.2" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 35.6L30 48.4" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More