Compare commits

...

168 Commits

Author SHA1 Message Date
bherbruck 37a586dd61 Release 0.25.1 2026-10-27 20:58:30 -04: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 a79f40a711 Release 0.25.0 2026-10-27 18:05:39 -04: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
bherbruck b5728f284b Release 0.24.1 2026-10-27 08:34:27 -04: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
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
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
Sloptimum 783ee91404 docs: a new label needs Arabic and Hebrew too (#3457) (#3458)
Arabic and Hebrew are complete catalogs since #3768, so
every_complete_catalog_carries_every_english_message fails a pull request
that adds an English message without them. The localization guide's list of
languages a new label needs now says so.

Co-authored-by: rootarchitect <7781866+rootarchitect@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:00:02 +00:00
taylorhayis 38dedcca3b Move/Copy can select from the browser (#4771) (#4774)
* claim: #4771

* Move/Copy takes a browser body the same way Combine does (#4771)

A body row feeds the dialog's active input through the same pick as a
click in the view, and does not replace the selection (#4452). A group,
the Bodies folder, or a component is that pick for every body in the
set. M and the Move/Copy button also start from a selected body or from
a component picked in the tree.
2026-10-27 05:23:56 +00:00
github-actions[bot] 99cca6a11a Regenerate generated files and fold pending files [skip ci] 2026-10-27 05:40:43 +00:00
ABDULLAH af07f4e0e5 Arabic interface language with right-to-left text (#3762) (#3768)
* claim: #3457

* Hebrew interface language with right-to-left text (#3457)

Adds a complete Hebrew catalog (assets/locales/he.json, original
translations) and registers `he`.

egui 0.36 shapes each font run with harfrust, so a run of Hebrew letters
is drawn right to left, but it places the runs of a line left to right in
stored order. The new `bidi` module runs the Unicode Bidirectional
Algorithm (unicode-bidi, MIT OR Apache-2.0) over each message of a
right-to-left catalog once, giving egui the runs in display order with
brackets mirrored and placeholders kept whole, while leaving each Hebrew
run's letters in reading order for the shaper. A test lays messages out
with a real Hebrew font and checks where the glyphs land.

Desktop builds append a system Hebrew font (Arial Hebrew / SF Hebrew,
Segoe UI / Arial, Noto Sans Hebrew / DejaVu Sans). Layout mirroring,
wrapped multi-line RTL paragraphs and a web Hebrew font are follow-ups.

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

* Hebrew: translate the new Describe Command label; list he in the control protocol

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

* Hebrew: catch up with main's catalog and the new ribbon (#3457)

- Translate the 258 messages main added since the branch was cut, drop
  the 38 it removed (10 renamed ones keep their translation) and
  retranslate the two whose English changed (toolbar → application bar).
- The timeline title is ציר הזמן, matching the glossary.
- Ribbon buttons split their labels into two lines from the start of
  the string; Hebrew labels come in display order, so the first line
  showed the end of the phrase ("חדשה / סקיצה"). For right-to-left
  labels the first line now takes the words at the end of the string,
  and a shortened line loses characters, and shows its ellipsis, on the
  left. The word-boundary sweep checks Hebrew from the lines the galley
  is built from, since epaint's right-to-left rows carry zero-width
  glyphs.

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

* Hebrew: after rebasing onto main, load the Hebrew face with the cached system fonts

main now searches the system fallback faces once (SystemFonts, on a thread);
the Hebrew face joins that search with its coverage check. Drop the three
camera-animation messages main removed (#4296) from the Hebrew catalog.

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

* Hebrew: translate the messages main added after the catch-up (#3457)

Translates the messages main added since the last catch-up: the history
bar's buttons and name toggles, the 3D mouse driver and axis mapping, the
thread size columns, the File view (home page), the redesigned Preferences, the new issue report
dialog, free orbit and the assistant's setup card. Drops the one message
main removed. The catalog has every English message again, so the
complete-catalog checks pass for he.

* Arabic interface language (right-to-left, on the Hebrew bidi path)

Adds an original Arabic translation of the whole English catalog
(assets/locales/ar.json, every message) and registers it. `ar` joins the
complete catalogs, so a message added to en.json later fails the build
until it is translated. It goes through the right-to-left path of #3458
unchanged: crate::bidi puts each message in display order and leaves
every Arabic word's letters in reading order for harfrust, which joins
them with the font's own tables. Ribbon labels use the right-to-left line
split from the same stack.

fonts.rs adds a system Arabic fallback after the Latin interface font,
which keeps spaces, digits and Latin text so each Arabic word stays one
shaped run: Segoe UI/Tahoma/Arial on Windows, SF Arabic/Geeza Pro/
Damascus/Tahoma on macOS, Noto Sans or Naskh Arabic/KACST One/DejaVu Sans
on Linux, or NotoSansArabic-Regular.ttf in CRAFT_FONTS_DIR. It is the first
fallback for an Arabic UI (Arial Unicode, a CJK candidate, has Arabic
letters without joining data) and the last otherwise; like Thai and
Hebrew it is never promoted for a CJK interface.

Tests: the catalog comes back in display order with no directional
controls, and Arabic laid out with a real font lands on screen word by word
in reading order.

* Hebrew: review the catch-up translations; restore the camera-animation messages (#3457)

- Review of the 533 messages translated in the previous commit: 21
  corrected. Keys in the motion dialog are keyframes (נקודות מפתח, as
  in Add Key), the Design workspace is תכן as in its label, a dropped
  "(Direct Driver)" is restored, terms aligned with the rest of the
  catalog (פורמט, סוכן קוד, רצועת הכלים), and a few calques reworded.
- Translate ui.animate_view_changes, ui.animation_speed and
  ui.look_at_anim_desc, which main has again, so he stays complete.

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

* Arabic and Hebrew: translate the messages main added since (#3457, #3762)

Both catalogs again carry every English message. The 760 messages main added
since the last rebase (render workspace, material editor, standard views,
sheet metal Path Flange, AI app connections, document-on-disk notices and
more) come from A-AL-ANAZI's translations on #3768; the five Perforate dialog
messages are new here. The earlier Hebrew review is kept on top of them, and
33 of the new Hebrew messages are corrected: "completed" was "perfect"
(מושלם → גמור), Regrade read as ranking (now תיקון צבע מחדש), shaders,
guarantee, the Mirror reflection mode, simulations and displacement.

Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Arabic and Hebrew: attribution rows as a pending file

main now takes new ATTRIBUTION.md rows as one file per change
(attribution/pending/), so the he.json and ar.json rows move there.

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

* Arabic and Hebrew: translate the 23 messages main added since (#3457, #3762)

Both catalogs again carry every English message; the two theme-page messages
main removed are dropped. Twelve of the new messages (link updates while
loading, the agent chat's model choice, plugin search, render quality) come
from A-AL-ANAZI's translations on #3768; the sketch plane descriptions, the
rigid group and empty-boolean messages, free sketch points and the web update
note are new here.

Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Arabic and Hebrew: translate the 12 messages main added since (#3457, #3762)

Eleven come from A-AL-ANAZI's #3768; "Turn Up Axis" in Hebrew follows the
catalog's existing up-axis wording, and the agent chat's no-tool-call message
is new in both.

Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Arabic and Hebrew: translate the 22 messages main added since (#3457, #3762)

* Arabic and Hebrew: a label found by its English text comes back in display order too (#3457, #3762)

* Arabic and Hebrew: translate the 32 messages main added since (#3457, #3762)

* Arabic and Hebrew: translate the 34 messages main added since (#3457, #3762)

* Arabic and Hebrew: translate the 30 messages main added since (#3457, #3762)

* Arabic and Hebrew: translate the 7 messages main added since (#3457, #3762)

---------

Co-authored-by: rootarchitect <7781866+rootarchitect@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Abdullah alanazi <x1700@MacBook-Air-Abdullah-2.local>
2026-10-27 05:05:53 +00:00
github-actions[bot] 0c16cfefbb Regenerate generated files and fold pending files [skip ci] 2026-10-27 04:40:23 +00:00
github-actions[bot] b2189e3f4d Regenerate generated files and fold pending files [skip ci] 2026-10-27 04:31:59 +00:00
Victor Santos Oliveira 6e12f5954f Navigation: snap free orbit direction and roll with Shift (#6943) (#6949)
* claim: #6943 free orbit Shift snapping

* fix(ui): snap free orbit direction and roll with Shift

* test(ui): align free orbit coverage and complete snap help translations
2026-10-27 04:11:16 +00:00
Jackson Oursland 3ad5a360f2 Engine: keep the model and placed world across appearance-only edits (#7396)
Evaluation and placement never read appearances, yet an appearance edit
re-evaluated the model and placed every occurrence again. A whole-document
proof that only appearances changed now takes the existing render-only refresh,
which keeps the model and retargets the world-placement cache.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 03:40:29 +00:00
Nathalie Schönwetter 6848c78424 Sketch Transform: one box with handles to scale, move and turn what is picked (#6004)
* Sketch Transform: scale, turn and move sketch entities in one command and one undo

sketch.transform scales about a centre, turns about it, then moves; the window and its
handles for existing sketch geometry build on it.

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

* Sketch Transform window: one box with handles to scale, move and turn what is picked

Select curves and points in a sketch and press G (or Transform in the Modify panel or the
context menu): the picks get a box with a handle at each corner to scale (the opposite corner
stays), the box to move and a knob above it to turn (Shift snaps to 15 degrees). Position,
Width, Height, Scale and Angle in the window are the same values, to type exactly; OK applies
it as one step.

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

* Move the new message to a pending locale file

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

* Sketch Transform: follow the sketch field of Sel::SketchCurve

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

---------

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 03:34:07 +00:00
DuBearte b47f82d406 Reopen the designs open at the last quit (#5542) (#6006)
* claim: #5542

* Reopen the designs that were open at the last quit (#5542)

A launch with no file to open reopens the saved designs that were open
when SolidCraft was last closed, in tab order. Designs never saved and
files that are gone are skipped. Preferences > General > Reopen designs
turns it off; a launch driven over --control or with --sample starts fresh.

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

* Move the new messages to a pending locale file

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 03:34:04 +00:00
Jackson Oursland 614e006eac Engine: keep every placed body's frame with the world state (#7411)
Each command that names a body (appearance, material, move, ...) mapped it to
its component's own name by working out every placed body's world name and
frame, so a command on a large assembly cost a walk of the whole design. The
list is now kept with the world-placement cache while it is current.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 03:33:53 +00:00
Jackson Oursland 4e07b6c4b7 Assembly: name placed bodies without rescanning the design (#7380)
Naming a placed body scanned every occurrence and body of the design, so the
world state and every caller naming all placed bodies grew with bodies times
design size. WorldNames indexes the design once and names each body in time
proportional to its occurrence path; one naming routine serves both it and the
single-body scan. Names are unchanged (a test compares them with the scanning
implementation).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 03:30:33 +00:00
Jackson Oursland efa2cae0b4 Viewport: draw faded and see-through bodies from their plans with OIT (#7423)
* Viewport: palette-indexed scene and incremental appearance updates

- Move triangle, line and instance colours and opacity into a per-scene
  palette (an R32Uint texture, WebGL2-safe); appearance edits update
  palette entries instead of vertex bytes.
- Build CPU body triangles as one canonical vertex stream per block with
  opaque/glass/faded index lists; an opacity class change rewrites only
  the index list.
- Cache resolved body appearances by a per-body key, so an edit resolves
  only the bodies whose look changed.
- Split the model scene key from the session revision: selection and
  view-only commands no longer rebuild or upload the model scene.
- Replace the O(n^2) section-index lookup and per-placement instance scan
  with hash lookups.

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

* Viewport: skip unchanged palette work when rebuilding

- Reuse each body's appearance key while the document is unchanged.
- Leave a palette slot alone when its value did not change, and allocate
  every body's slots once before the build loop.

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

* Viewport: measure scene rebuild cost by edit kind

An ignored scale test times the scene build after selection, appearance,
theme and one-body geometry edits at 200, 1,000 and 5,000 bodies.

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

* Viewport: read faded grid alphas through the scene palette

The grid-fade tests now resolve line palette indices instead of reading the
line record's old colour byte.

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

* Viewport: measure rebuild cost on few large meshes

Adds the 20/100-torus case where a block rebuild used to re-emit a large
part of the model.

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

* Viewport: measure rebuild cost on an assembly

Unique detailed parts plus one component placed many times, timed for
selection, part and component colour, opacity, theme, hide and move.

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

* Viewport: draw block classes as vertex runs, not index lists

A body's vertices are three per triangle in mesh order, so each draw class
is a few runs of the block's vertex buffer: all-opaque blocks need no index
buffer, and an opacity class change uploads nothing. A fragmented class
(more than 64 runs) falls back to one index buffer.

Each triangle is written as one record into a block reserved up front,
which takes the cold scene build of a 7.1M-triangle assembly from 97 ms
to 55 ms (main: 83 ms).

Also fix reindexing a block whose bodies are drawn elsewhere: it walked
the mesh's triangles from that body's start, past its own vertices.

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

* Viewport: draw the highlight's own block, and light faded bodies without copies

The highlight scene keeps its feedback (a hovered face, a body copied on
the CPU) in a block of its own; since blocks became vertex runs, only the
model's blocks were drawn, so that feedback was uploaded and never shown.
Draw the highlight's blocks with its triangles.

Whole faded and see-through bodies (another component active, or an
appearance below full opacity) were lit by copying every triangle on the
CPU at each hover change: 85 ms for a design row of 812 bodies. They are
now drawn again from the model's own block vertices, in the highlight
palette with their slots set to the highlight colour, or as placements of
the model's shared triangles. The picture is the same as the copy's.

Adds a GPU frame benchmark for document-tree hover (ignored test).

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

* Viewport: count scene uploads, and keep the sorted glass stream across colour edits

Renderer::uploads reports the bytes the model scene's last change uploaded
(geometry, placements, palette rows, planned colours, re-evaluated vertices).
Without order-independent transparency the global glass stream is kept while
the blocks' see-through triangles are the same, so a colour edit on a glass
body uploads palette rows only. Tests assert both on a GPU.

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

* Viewport: keep palette slots and looks without rehashing every body

A rebuild found every body's palette slots by name and wrote them back,
allocating and hashing a new key per body, and copied every shown name into a
set to check the arena still belongs to the design. Slots are written back only
when a body's faces grew, and the looks cache is pruned only when it holds more
bodies than are shown. Adds the STEP-file benchmark (SOLIDCRAFT_SCALE_STEP) and
the colour command's own time to the assembly benchmark.

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

* Viewport: round-pick construction lines through the scene palette

The axis and centre feedback for round picks (#4610) drew with a raw colour;
on the palette scene it is interned once like the other construction geometry.

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

* Viewport: draw faded and see-through bodies from their plans with OIT

A body faded outside the active component, or see-through, left the GPU
tessellation path and was built triangle by triangle on the CPU, so working in
a component of a large assembly rebuilt every other body. Where the renderer
blends see-through surfaces order-independently (GpuTarget::oit, set by the
renderer), a body whose faces share one class is drawn from its plan: chunks
are split by class, opaque passes, AO and the section draw only opaque chunks,
and the OIT pass draws the rest. The sorted fallback keeps them on the CPU.
The model and its highlights decide through one rule (model_plan_class).

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-27 03:27:32 +00:00
github-actions[bot] 78d80e0144 Regenerate generated files and fold pending files [skip ci] 2026-10-27 03:52:42 +00:00
CarelT 13f650b695 Engine: one module for the design-link path helpers (no behaviour change) (#5420)
* Move the design-link path helpers into one module

`component_link.rs`'s path rules for links to other design files (base_dir,
absolute, relative, stored, resolve, rebased) move, unchanged, into
`cmd/design_link.rs` with their test, so other kinds of link (#3891's drawing
files, linked parameters #5354, #5113's extract) share them instead of copying
or re-exporting them. No change in behaviour.

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

* design_link: keep base_dir and relative private

Only design_link.rs uses them; the module's surface is absolute, stored, resolve and rebased (review on #5420).

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

* component_link: import Document after the rebase

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

---------

Co-authored-by: Carel Theron <careltheron@Carels-MacBook-Pro.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 03:10:16 +00:00
Victor Santos Oliveira a7c1909766 CLI: evaluate scripts on the input design (#6861) (#6872)
* claim: #6861 eval scripts on input designs

* fix(cli): evaluate scripts on the input design (#6861)

* fix(cli): validate eval options before opening inputs

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:55:01 +00:00
Colin Faulkingham 702f35ae6e Loft: keep hidden bodies out of automatic Join targets (#1988) (#6894)
* claim: #1988

* fix: preserve hidden bodies in automatic loft joins (#1988)
2026-10-27 02:54:56 +00:00
Lars Jansen 2d629069c6 Dialogs: OK and Cancel at the top (#6708) (#6713)
* claim: #6708

* feat(dialogs): OK and Cancel at the top (#6708)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:42:23 +00:00
Homero Thompson 4b327bab1f Canvas: the value boxes do not cover the command window (#6608) (#6661)
* claim: #6608

* Canvas: keep value boxes clear of the command window (#6608)

* Canvas: describe how value boxes clear the command window (#6608)

* Canvas: keep the value boxes clear of the gizmo handles (#6608)

* ui-egui: format the value box regression test (#6608)

* gpu: the startup MSAA check skips when there is no GPU adapter

The check needs a real adapter to draw the multisampled view and to
resolve it, so on a machine with no software Vulkan adapter it is
skipped, with the reason printed, instead of failing the build.
SOLIDCRAFT_REQUIRE_GPU=1 still makes a missing adapter a failure, as in
the CI job that has a software adapter.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:29:53 +00:00
github-actions[bot] 237341423c Regenerate generated files and fold pending files [skip ci] 2026-10-27 02:49:11 +00:00
dudedagger42 b99761a3f7 Extrude: a cut made while bodies are hidden leaves them alone (#3961) (#5148)
A cut (or intersect) that names no bodies went through every body it met,
hidden ones too: a body hidden to keep it out of the cut was cut anyway. When
some of the active component's bodies are hidden, the cut now stores the shown
ones as its targets, so the hidden bodies stay as they are, and stay so once
shown again (the model never depends on what is shown). With nothing hidden it
goes through every body it meets, as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:23:48 +00:00
DuBearte 3726959d82 Control channel: check the token before parsing the whole request (#6643)
Until a connection had shown the token, its first line (up to 16 MiB)
was parsed whole into a JSON value. A line of [0,0,...] that size is
hundreds of MB once parsed, so 16 connections without a token could
make the app run out of memory and lose unsaved work. The token is now
read from the request object without building the rest of it: other
fields are only skipped, and anything but an object with a string
token is refused as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:04:28 +00:00
HotThrower71 af1d15b5ea Canvas to Sketch: trace a canvas image into a sketch on it (#3931) (#4263)
* Canvas to Sketch: trace a canvas image into a sketch on it (#3931)

New command canvas.to_sketch, offered as Canvas to Sketch in the canvas
context menu. It traces the canvas image (automatic Otsu threshold, light
drawings on dark backgrounds inverted) with interpolated marching squares
and Douglas-Peucker, and adds lines and cubic Bezier splines through Insert
SVG's add_geometry to a new sketch on the canvas plane, with the canvas'
width, centre, rotation and flip. Holes stay holes; one undo step.

The image's declared width x height is checked before decoding (at most
40 megapixels) and the decoder runs under image::Limits, since canvases
are capped by file size, not pixel count.

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

* Move the new message to a pending locale file

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 01:39:18 +00:00
github-actions[bot] 84a3a0c478 Regenerate generated files and fold pending files [skip ci] 2026-10-27 01:57:22 +00:00
github-actions[bot] 1551c0acef Regenerate generated files and fold pending files [skip ci] 2026-10-27 01:51:03 +00:00
Lars Jansen f1cc49bcdb History: list a sketch under the feature built from it (#6489) (#6494)
* claim: #6489

* feat(history): list a sketch under the feature built from it (#6489)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:59:57 +00:00
Homero Thompson 622a392a83 Extrude to Object: a Join extrude also joins the target body (#6424) (#6471)
* claim: #6424

* fix: a join extrude also joins the body it targets (#6424)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:53:25 +00:00
phanoob 5622963b91 Insert SVG / DXF: place, size and turn the drawing before it is added (#2605, #3443) (#3542)
* claim: #2605

* Insert SVG / DXF: place and scale the drawing before it is added (#2605, #3443)

After the file is picked, an Insert SVG (or DXF) window sets the
drawing's X, Y offset and scale, with a live preview; OK adds it in one
step and Cancel adds nothing. The insert commands already took `at`
and `scale`; the window is what was missing.

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

* Insert scenario: the picked drawing goes through the placement window (#2605)

`sketch_toolbar_imports_use_file_picker` expected the SVG added as soon as
it was picked; it now checks the placement window first and adds it with
OK.

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

* Insert SVG / DXF: a size in millimetres, an angle, and placement by the drawing's centre

sketch.insert_svg and sketch.insert_dxf take width or height (mm, aspect kept, winning over
scale), angle (degrees) and align (origin or center). The new sketch.drawing_bounds reads a
file's own size without inserting it.

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

* Insert SVG / DXF window: Width, Height and Angle, placed by the drawing's middle

Width and Height in millimetres follow the scale (aspect kept); Angle turns the drawing
about its middle, where X and Y now put it. Uses sketch.drawing_bounds for its own size.

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

* Insert SVG / DXF: handles in the viewport to scale, move and turn the drawing

The preview gets its box with a handle at each corner (scales about the opposite corner,
aspect kept), the box itself (moves) and a knob above it (turns, Shift snaps to 15 degrees).
They edit the same X, Y, Scale and Angle the window shows.

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

* Insert handles: the move that comes with the release counts

A quick drag could stop short of the pointer's last position because the final move arrives
in the same frame as the release.

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

* Insert SVG / DXF: the Drawing Size command's label in the catalogs

`sketch.drawing_bounds` came without a catalog entry, which the i18n
test requires (and the complete ja and zh catalogs with it).

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

* Move the new messages to a pending locale file

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

---------

Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-27 00:50:35 +00:00
github-actions[bot] 3bea744823 Regenerate generated files and fold pending files [skip ci] 2026-10-27 01:25:12 +00:00
N3MI-DG e8eb7ee11a Components: copy and paste bodies and components between designs and components (#3390) (#3390)
Squashed for the rebase onto main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:33:57 +00:00
yhamil04 ebb6cc53ab Crash reports: capture native crashes on macOS (#2113) (#6593)
* claim: #2113

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

* Crash reports: capture native crashes on macOS (#2113)

A signal handler for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP and SIGABRT
writes the same <journal>.native.json as the Windows exception filter: the
signal, the faulting module and offset, the thread and a frame-pointer walk.
The next start reports it instead of "an unclean exit". The signal then goes
on to the previous handler or the default action, so macOS still writes its
own crash report.

native_crash_macos.rs joins the crash-safety skill's list of files that may
use unsafe (no safe API can catch these signals).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:14:00 +00:00
github-actions[bot] 924724dc89 Regenerate generated files and fold pending files [skip ci] 2026-10-27 00:34:00 +00:00
HiroKenEz 56bcd872cb Cone primitive: solid.cone with base and top radius (#5641) (#6250)
* claim: #5641

* Cone primitive: solid.cone with base and top radius (#5641)

A cone or truncated cone placed like the cylinder: base centre, axis,
base radius (or diameter), top radius (0, the default, is a full cone)
and height, with the new body / join / cut / intersect choice. It is
built from what the kernel already exposes, a revolve of its half
section about +Z through the origin, moved into place. Toolbar entry
after Torus, a Cone dialog that also places it on a picked face, Edit
Feature, a timeline icon, pending UI strings and attribution, and one
pending design-format change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:57:43 +00:00
yhamil04 6c0fca30a7 Press Pull on a fillet's round face edits the fillet (#1932) (#6815)
* claim: #1932

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

* Press Pull on a fillet's round face edits the fillet (#1932)

Q on a fillet's round face opened Offset Face, which refuses a face
between two others it is tangent to ("a corner whose faces don't meet
the same way after the move"), so OK stayed disabled. The engine's
solid.press_pull already changes the fillet's size on such a face; the
window now does the same and reopens the fillet (or chamfer) to edit.
Other curved faces still open Offset Face, planar faces Extrude.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:34:09 +00:00
yhamil04 ef1d02f274 Claim bot: let an unassigned claimant /unclaim (#6718) (#6719)
* claim: #6718

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

* Claim bot: let an unassigned claimant /unclaim (#6718)

GitHub doesn't assign people who aren't collaborators, so many claims
have the `in progress` label and the bot's "Claimed by @user" reply but
no assignee, and `/unclaim` refused the claimant. When nobody is
assigned, `unclaim()` now also accepts the user named in the bot's
latest "Claimed by" reply on the timeline, unless the label was removed
after it. Maintainers and the owner rule are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:28:46 +00:00
github-actions[bot] 367aa249bc Regenerate generated files and fold pending files [skip ci] 2026-10-27 00:07:22 +00:00
The Nomad afd6a5c584 Send to Slicer: send the selected bodies to a slicer (#2595) (#3600)
* Add Open in Slicer for the selected bodies (#2595)

One command saves the selected bodies as a 3MF in millimetres (an STL if the 3MF can not be written) and opens it in the last slicer used. It is on the File menu, the Mesh tab and the body right-click menu. The first time, or when that slicer is gone, it asks which one with the same list Save as mesh uses. The Save as mesh dialog is unchanged.

* Send slicer files to a private per-user folder (#2595)

Files sent to a slicer used to go to a shared folder in the temp directory. They now go to the user cache folder (mode 0700 on Unix), under a name that stays the same for the same design and bodies, so sending again replaces the file and the slicer can reload it. The file is written to a temporary name and renamed into place. If the name can not be replaced, as on Windows while the slicer holds the file open, the copy gets a number and the status line says so. Files older than seven days are deleted at startup. The first send tells the user that sending again replaces the open file, and so does the tooltip of Open in Slicer. Send to slicer in Save as mesh uses the same path.

* Open files in a running slicer (#2595)

PrusaSlicer, SuperSlicer, Bambu Studio and Cura are started with --single-instance, so a slicer that is already open loads the file instead of opening a second window. OrcaSlicer has no such flag and is left alone, and macOS bundles are single-instance already. PrusaSlicer 3.0 ships a launcher entry that already carries --single-instance-on-url; it keeps that one and never gets a second flag. It is a preference, on by default, and scripts can pass single_instance. Preferences also gets a button to forget the slicer Open in Slicer remembers.

* List slicers the desktop entries say open 3MF or STL (#2595)

On Linux the scan of desktop entries now also lists a program that claims 3MF or STL in MimeType, even when it is not in our table of known slicers. The table still supplies names, ids, icons and flags for the ones it knows. Entries marked NoDisplay or Hidden are skipped, and so are terminal programs and SolidCraft itself. The existing size and count limits stay. The Windows scan now looks inside a Prusa3D folder, where PrusaSlicer installs itself.

* Only start slicers the user can see or chose (#2595)

file.save_mesh took a slicer parameter that could be any executable path, so a script or an MCP client could run any program. The slicer now has to be one of the detected slicers (by id or name) or a program the user already added under Custom, which the app hands to the engine from its settings. The same rule holds for file.open_in_slicer. The agent chat already asked before any file command; its question now also names the slicer.

* Review fixes for Open in Slicer (#2595)

The file name hash keeps all 64 bits instead of 32. When Open in Slicer has to fall back to an STL, the status line now says so. The engine test that sets the chosen programs clears them again. The unused "could not make the folder" message is gone from the catalogs, and the docs say plainly that finding slicers by file type is Linux only.

* Keep unsaved designs apart and hide Open in Slicer in the browser (#2595)

Two unsaved designs with the same name and body names got the same file in the slicer folder. Every document now has a uid that is new for each New, Open and Recover and stays the same for the life of the document, and the file name uses it (with the process id) until the design has a path. At the first Save As the name changes to the path-based one, so the slicer gets one new file; the old one goes with the seven-day sweep.

The browser can not start a program, so Open in Slicer is now disabled there: the engine command reports why, the File menu item and the body and component right-click items are greyed, and starting it anyway says so on the status line instead of opening a dialog. The Save as mesh dialog keeps its browser guard for the open flow too.

* Put the Session doc comment back on Session (#2595)

The new NEXT_UID static had landed between the doc comment and the struct it describes.

* List every install of a slicer, and test the first Save As (#2595)

Detection kept one entry per known slicer, so a system PrusaSlicer 2.9 and a Flatpak 3.0 beta showed up as one. Every distinct install is listed now. The same program found twice, through a desktop entry and through PATH, is still one. When a slicer has several installs, each gets an id made from where it was found (prusa#comprusa3dprusaslicer) that stays the same on every run, and a name worked out from its command and location without running anything: PrusaSlicer (system), PrusaSlicer (Flatpak, beta), PrusaSlicer (AppImage). A lone install keeps its plain id and name. The plain id or name still picks the first install, so a remembered "prusa" keeps working. Single-instance flags and the other per-slicer behaviour go by the id without the install part.

Also a test that the first Save As changes the file name once and later sends keep the name that comes from the saved path.

* Start PrusaSlicer 3.0 first and send the file once it is ready (#2595)

PrusaSlicer 3.0 starts through slic3r-app-launcher, which ignores a file given at a cold start whatever the flag, and loads it over D-Bus when an instance is already running. A small table in slicers.rs names such slicers by their command; today it holds that launcher, so PrusaSlicer 2.9 and OrcaSlicer are untouched. Ready means a name starting with com.prusa3d.prusaslicer.BroadcastListener. is on the session bus, read from busctl --user list with no new dependency. If busctl is missing or fails, nobody waits and the file is sent as before.

When the slicer is not ready, Open in Slicer starts it without the file and looks every 200 ms for up to 10 seconds. The wait runs on another thread, so the window stays live, and the status line says Starting PrusaSlicer. Once the name appears the file is sent with the same arguments as before. If the limit passes, the status line says the slicer did not respond, that the file is already saved, and to send again. A second click during the wait only repeats the Starting line, so there is one launch and one send. Scripts calling file.open_in_slicer wait inside the command by default, or pass wait: return or no.

* Always show the slicer picker for Open in Slicer (#2595)

File > Open in Slicer, the Mesh tab entry and the right-click entry now always open the picker, with the slicer used last time pre-selected, so the usual case is one click or Enter. Scripts and file.open_in_slicer with a slicer still send without a dialog. The preference button that forgot the remembered slicer is gone: it could no longer make anything ask again, and it did not clear the pre-selection either. The guide says how it works now.

* Leave Bambu Studio out of the single-instance list (#2595)

Open files in a running slicer now adds --single-instance for PrusaSlicer, SuperSlicer and Cura only. Bambu Studio is started as it is, so it may open a second window. A test pins that, for the detected slicer and for one the user picked as a custom program. The guide and the preference tooltip no longer name Bambu Studio as getting the flag.

* Sort the English catalog and leave features.md to the generator (#2595)

cargo xtask lists wanted one message moved in en.json after the rebase. docs/features.md is written by the Generated files workflow on main, so the row I had added by hand is gone from this branch; the workflow adds Open in Slicer when it lands.

* Drop the unused folder message from the Vietnamese catalog (#2595)

Vietnamese arrived on main after I removed ui.save_mesh_temp_failed from the other catalogs, so the check that every translated id exists in English failed on it. Nothing uses the message any more.

* Say so when a started slicer can not be found again (#2595)

After Open in Slicer had started a slicer on its own, a failure to look it up again to send the file just returned, so the status line stayed on Starting and then nothing. It now says the slicer was started, that it can not be found to send the file, and where the file is saved. A test covers it.

* Say why a failed busctl ends the wait (#2595)

If busctl fails partway through a wait, the wait stops and the file is sent, the same as when busctl can not be run at all. That is on purpose: a file sent too early is dropped quietly and costs one more send. A comment in wait_for_ready says so.

* Keep the readiness wait inside its limit (#2595)

wait_until asked the probe before checking the deadline, including right after a sleep, so a yes could be accepted after the limit, and busctl ran with no timeout, so one look could outlast the whole wait. The deadline is checked first now, the probe is told how much time is left, and busctl is given at most two seconds (less near the end of a wait) and is killed after that. A hung busctl counts as not telling, which sends the file as before. This covers the first look on the UI thread too. Tests cover a slow probe, the shrinking time left, and a fake busctl that hangs, answers and fails.

* Send the file that was already written once the slicer is ready (#2595)

When the wait for a started slicer ended, the UI ran file.open_in_slicer a second time. That exported the design again on the UI thread, from whatever the session looked like by then, and could write a numbered copy while the timeout message still named the first file. The wait now keeps the slicer it found and the answer of the first call, and opens the file that was written. A test changes the design during the wait and shows the same file is sent, is not written again, no command runs, and no second file appears.

* Send with the single-instance flag after starting a slicer (#2595)

After Open in Slicer has started a launcher that needs a start first, the file has to reach the instance that is now running, so the send that follows a start carries --single-instance whatever the preference says. A normal send, to a slicer that was already up, still follows the preference. A comment in both places says why. The test uses a plain launcher command with the preference off: the normal send has no flag and the send after a start has it.

* Find the remembered slicer again after an uninstall (#2595)

With two installs the remembered key is prusa#prusaslicer. Remove the other install and the lone one is plain prusa, which matched neither the exact key nor the plain-key fallback, so the picker fell back to the first slicer in the list, maybe a different one. The key is now also matched by the part before the #, so the same slicer is pre-selected. A test covers the lone install, a plain key with two installs, an exact key, and that another slicer is never picked.

* Say a second click did not send anything while a slicer starts (#2595)

While a started slicer is being waited for, another click showed Starting <the first slicer> even when it had picked another slicer or other bodies. It now says it is still waiting for that slicer and that nothing else was sent. The test checks the wording and that it names the slicer being waited for.

* Label macOS installs by their application (#2595)

An open -a command resolved its program to /usr/bin/open, so every application on a Mac was labelled system. Labels for open commands now come from the .app path: /Applications is system and a folder under home is shown as ~/Applications. A test with two PrusaSlicer.app bundles and a made-up home checks both labels and that the ids differ.

* Pick the same place for a program found twice every time (#2595)

When two desktop files, or a desktop file and PATH, pointed at one program, the first one read was kept and its file name went into the id, so the ids could change with the order the folders were read in even though nothing was installed or removed. The place that sorts first now speaks for the program. A test with two aliases and a second install lists them with the folders in both orders and gets the same ids and commands.

* Treat flatpak run spelled two ways as one install (#2595)

flatpak run ... and /path/to/flatpak run ... with the same arguments had different install keys, so one invocation could be listed twice. The launcher is now compared by its file name and the arguments are kept, so both give the same key, while a different branch is still its own install. The test uses a real launcher file so the full-path entry is not skipped as stale.

* Say plainly that a late yes is accepted on purpose (#2595)

The comment on wait_until said a slow probe could not accept a yes that came too late. That was not what the code does: no probe is started after the limit, but one already running may finish and its yes is taken, because the file should still go. The comment now says so, with the bound: the limit plus that last probe, which in production is given the time left and so ends by the limit. A test has a probe that runs past the limit and says yes, and checks it is accepted and the wait stays within limit plus the probe time, and that a probe that says no is not followed by another.

* Give reading busctl the same deadline as running it (#2595)

After busctl exited, the output was read with a join that could wait forever if a process it left behind kept the pipe open. The reader now hands the text back over a channel and the wait uses the time that is left of the same limit, and gives up with nothing otherwise. The polling sleep no longer overshoots the time left either. A test uses a fake busctl that backgrounds a sleep with its output inherited and exits at once: the call returns within its limit.

* Tell two different flatpak launchers apart (#2595)

Comparing only the file name of the launcher made /opt/tool-a/flatpak run ... and /usr/bin/flatpak run ... with the same arguments one install even when they are different programs. The launcher is now found the way any program is, through PATH and with links followed, and its name stands in only where it can not be found. The test has flatpak on PATH and the same file by its path as one install, a different executable of the same name as a second, a different branch as a third, and the fallback when there is no PATH.

* Keep waiting when busctl is slow, send only when it is missing or fails (#2595)

Since the busctl timeout, a slow busctl came back as "can not tell", and that was read as send now. One slow look, likely just when a cold PrusaSlicer 3.0 is starting under load, ended the wait and sent the file early, which gets dropped. The look now says why it has nothing: slow means not up yet, so the wait goes on to its limit and then reports that the slicer did not respond; a missing or failing busctl still means it can not be told and the file is sent. The comments name only those two cases. Tests use fake busctl programs: one that hangs keeps the wait going to the limit and times out, a missing one sends at once, one that says not up and then fails sends, and one that names the slicer is ready.

* Do not open an empty path when the answer had no file (#2595)

If the answer kept for a started slicer had no path, the send ran open with an empty string. The tick now says there is no file to send and starts nothing. The test gives a wait with no path, checks the error line, and that the fake slicer was never run.

* Drop the unused folder message from the Danish catalog (#2595)

Danish arrived on main with ui.save_mesh_temp_failed, which this branch removed from English because nothing uses it any more, so the check that every translated id exists in English failed. The entry is gone from the Danish catalog too.

* Always start the 3.0 launcher with the single-instance flag (#2595)

A hand run showed that starting the PrusaSlicer 3.0 launcher with no file while a 3.0 is already running forwards to it and exits when the flag is there, but opens a second full window when it is not. A slow first look at the bus can start the launcher twice, and with the preference off the flag was stripped. A slicer that has to be started first now keeps the flag on that start whatever the preference says, the same reasoning as the send after a start; when nothing runs the flag does no harm. A normal send with the preference off still has none. The test covers the launcher entry, a bare launcher command, a normal send and a slicer that is not on the table.

* Swap --single-instance-on-url where it does not hand the file over (#2595)

A hand run on the PrusaSlicer 2.9.6 Flatpak showed that its entry's --single-instance-on-url does not forward a file to the running instance: every send opened a new one, three after a cold start and two sends. With plain --single-instance the second process finds the first, forwards, and exits. So when a command carries --single-instance-on-url and the slicer is not on the wait table, that is, not the 3.0 launcher, it is replaced by --single-instance, with the file markers handled as before, when the preference is on, and stripped when it is off. On the 3.0 launcher the flag does forward and stays. The comment on the rule says why. A test uses the real 2.9.6 and 3.0 Flatpak command lines.

* Cut long slicer names to fit the picker (#2595)

With installs listed on their own, a name like PrusaSlicer (Flatpak, master) is wider than its row. The list painted it with no limit, so it ran out of the popup, and the closed selector wrapped it onto a second line. The list now lays the name out on one line that ends in an ellipsis when it does not fit, the selector truncates, and the full name is in the tooltip when it was cut (for a program the user added, ahead of the delete hint). A test checks that a long name is cut to one line within the width and a short one is left alone.

* Keep the 3.0 launcher's flag on a send whatever the preference says (#2595)

A hand run with the single-instance preference off showed the 3.0 launcher needs its flag on a send too, not only on a start: a cold start with a file and no flag loaded nothing, and a send to a running 3.0 left a launcher process that never forwarded. A slicer on the wait table now gets the flag on every launch, as it already did when started alone. The doc comment says why. The preference text, in English, Japanese and Chinese, and the guide now say that PrusaSlicer 3.0 is the exception to turning this off. The tests that took the flag away with the preference off now expect it, for the launcher entry, a bare launcher command and the 3.0 Flatpak line; the test of the send in the UI checks the first send carries it.

* Keep the install-flag swap to PrusaSlicer, found by what it is (#2595)

The swap of --single-instance-on-url for --single-instance ran for any program whose command had the flag, so another program with that flag would have been rewritten. It is now a PrusaSlicer rule: it applies to a slicer recognised as PrusaSlicer by its identity, its id or, for a program the user picked, the name it goes by, never by a version number. The single-instance table uses the same identity check. A test keeps the flag of a program that is not PrusaSlicer, strips it when the preference is off, and swaps it for a picked program named PrusaSlicer.

* Do not promise a hard bound in the wait comment (#2595)

The comment on wait_until said the production probe ends by the limit. Its clock starts after the process is spawned, and killing and reaping the process and scheduling are not bounded by it, so it is a polling and read budget with process overhead on top. The comment says that. Comment only.

* Tell PrusaSlicer 2 from 3 by the installed version (#2595)

PrusaSlicer 2 and 3 need different handling, and the rules had to guess from the command line. Each install of PrusaSlicer now carries its major version, read when it is detected from the release at the top of its appstream metadata and never by running the program: a Flatpak's own files (the branch the command names, else the current one), or the metainfo file beside a desktop entry. PrusaSlicer older than 3 is sent the file with --single-instance in place of --single-instance-on-url, which does not hand a file over there, and is not waited for. PrusaSlicer 3 and newer keeps the launcher's flag, is started on its own, waited for, and sent the file. When the version can not be read, the command decides as before: the slic3r-app-launcher launcher counts as 3, and any other entry is left as it is. A program that is not PrusaSlicer is untouched whatever its flags. Tests use metadata fixtures, no Flatpak: 2.9.6, 3.0.0-alpha10, a branch that is not installed, a system package, an unreadable version with and without the launcher, and a non-PrusaSlicer program with the same flag.

* Make the busctl wait tests steady under load (#2595)

One of them failed once in a full run at its first assertion and passed alone. A script that has only just been written can fail to start (text file busy) while another test forks, and that reads as a busctl that is missing. The fake programs are now started once, patiently, with a flag they answer at once, before a test relies on them; a hanging one uses exec so it is killed cleanly. The time bounds that could trip on a busy machine are several seconds now, and the checks are on outcomes: not ready and keeps waiting, send now, and the number of looks. Thirty runs alone, thirty of the whole slicers filter and thirty of the busctl tests under eight busy loops all pass.

* Say what the busctl reader thread leaves behind (#2595)

The thread that reads busctl output is left to end when the pipe closes. A busctl that answered but left a process holding its stdout reads as slow, and every poll of such a bus can leave one blocked reader behind until that process lets go. A comment next to the thread says so. Comment only.

* Make Save as mesh a plain export (#2595)

Save as mesh asked for the bodies and the format, and could also send the file to a slicer with a checkbox and a slicer list. Sending now has its own command, so the checkbox, the list and the Send label are gone from this dialog: bodies, format, a file dialog, Save. The two settings that only the checkbox used, save_mesh_send and save_mesh_slicer, are no longer kept; a settings file that still has them loads, and they are ignored. The strings only they used are gone from the catalogs. The slicer list stays in the dialog of Open in Slicer. file.save_mesh keeps its slicer parameter for scripts. The guide describes the two as separate.

* Rename Open in Slicer to Send to Slicer (#2595)

The command sends a file to a slicer rather than opening one, and a later Send to App can share the name. File menu, Mesh tab, right-click menus, the dialog button, status lines, the guide and the English, Japanese and Chinese strings now say Send to Slicer, and the command id file.open_in_slicer is file.send_to_slicer. It is new in this change, so there is no alias. The remembered slicer is stored as send_slicer, and the question the agent chat asks says send to. Module and type names stay as they are.

* Put the send targets in one table and one send pipeline (#2595)

The known slicers, the list of slicers with a single-instance flag and the rule for slicers that must be started and waited for were three separate things. They are one table of targets now: id, label, kind, names and program files, accepted formats and the default, how the file is handed over, the single-instance flag, the version from which PrusaSlicer's on-url flag hands a file over, and the wait-for-ready rule. Programs that are not in the table are slicers with the same defaults. The kind is slicer, the only one so far; its comment says it is where a later Send to App would plug in. The send itself is one function: write the bodies, find the target, start it and wait if its rule says so, hand the file over, report. Detection and resolution take a kind. Nothing changes for what already worked.

file.save_mesh keeps its slicer parameter, now through the same function, with its export arguments and result unchanged: a cold PrusaSlicer 3 is started and waited for before the file goes, and it accepts the program path of a detected slicer as file.slicers lists it (file.slicers gives it as program). A path is accepted only when it is the program of exactly one detected target, and that target's own command and arguments run; a launcher several entries share, such as /usr/bin/flatpak, is refused as ambiguous, and any path nobody listed or chose is refused. The parameter and the code say it may be deprecated later in favour of file.send_to_slicer or a Send to App workflow, which is a possibility and not a plan.

Tests go through file.save_mesh itself for the two reported failures, a listed AppImage path and a cold PrusaSlicer 3 that is waited for, with the slicers and the busctl made up; and they check that the result and the file are what an export without a slicer gives, and that a path that is not listed, or is shared by two entries, writes nothing. The pipeline has its own tests: a cold launcher waited for, one already up, wait return, and one that never comes up. A test of save_mesh that shared a file name with another is given its own.

* Keep an AppImage's id through an upgrade (#2595)

When a slicer has several installs, each gets an id made from where it was found. For an AppImage that was its file name, which carries the version, so upgrading it changed the id and the remembered choice missed. The id is now made of the slicer's id, the fact that it is an AppImage and a short hash of its folder, so a new version that replaces the old one in the same folder keeps the id, and the same AppImage in another folder is its own install. Two versions kept side by side in one folder are two installs: while both are there, their file names are added to their ids to tell them apart.

A remembered id is found again only when it names exactly one install. An id that was one AppImage and is now two versions, or a plain id that is now several installs, is treated as not remembered, so the picker starts from the first entry as on a first run and does not quietly pick another install; a single install left of the same slicer is still found, as before. Installs that are not AppImages are told apart as before. An id from before this change no longer matches. Tests cover an upgrade, two versions side by side, a second folder, and the picker's side of each case.

* Add Open with EasyPrint to Send to Slicer

EasyPrint is Printables' slicer in the browser. It's a plain text row in
the Send to Slicer dialog, backed by a new file.open_in_easyprint
command. The command exports the bodies as a 3MF (or STL), hands them to
the host to upload to slicer.printables.com/import, and the app opens the
link that comes back. The desktop app uploads with ureq and the browser
build with fetch, so this is the one send target that works on the web.

The model leaves the machine, so the command refuses without
consent: true, the dialog asks once and remembers, and an AI agent's
request asks first. A model over 100 MB is refused before anything is
sent, and a failed upload shows as an error with the reason.

The chat's privacy note said SolidCraft sends nothing anywhere, which
isn't true any more, so it now says that about the chat. New strings are
in every catalog.

* Place Send to Slicer on the Insert Files panel
2026-10-26 19:59:35 -04:00
Frank 65366ba131 Move: after picking a body, go on to From, Point or Axis (#6170) (#6171)
* claim: #6170

* Move: after picking a body, go on to From, Point or Axis (#6170)

In Point to Point, Point to Position and Rotate, once a body is picked
and the next input is empty, the dialog goes on to it, so the next click
picks the point or axis instead of toggling another body. Free Move
stays on Bodies: its Pivot is optional.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:17:37 +00:00
DuBearte f077f48318 Sketch: a changed length dimension keeps its start point, so a side grows from its first corner (#6070) (#6139)
* claim: #6070

* Sketch: a changed length dimension keeps its start point, so a side grows from its first corner (#6070)

Changing a dimension's value re-solved its sketch as the smallest move from
the stored positions, which splits the change between both ends: a
rectangle side set from 40 to 60 mm grew 10 mm each way. parameters.change
now first solves a sketch whose length dimension (distance, horizontal or
vertical distance, line length) the parameter drives with that
dimension's first point held, and stores the result, so evaluation has
nothing left to move. Where holding it can't solve (a symmetry moves both
ends), evaluation solves as before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:03:31 +00:00
Dahak Builds bd44f6f496 Reference features by id everywhere: Pattern/Mirror seeds and construction planes (#1134) (#6858)
* claim: #1134

* doc: key pattern/mirror seeds and construction planes by feature id (#1134)

* engine: resolve seed names the way evaluation did; keep timeline.edit name input (#1134)

* fmt/clippy cleanups (#1134)

* ui: resolve construction-plane names by feature id in plane_description (#1134)

* fmt: sketch_attachment_ui after rebase adaptation

* engine: timeline.add resolves legacy name-keyed seeds and plane names (#1134)

* chore: re-trigger CI (GPU runner timeout flake)

* doc: replayed_sources walks id-keyed pattern seeds (rebase adaptation)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 22:56:06 +00:00
DuBearte 3918f23c3d Browser: a feature's eye shows or hides the bodies it made (#5099) (#6109)
* claim: #5099

* Browser: a feature's eye shows or hides the bodies it made (#5099)

A row in the Browser's Features folder has an eye when the feature made
bodies that are in the design now (a patch, a box, a pattern's copies;
not a fillet, which changes bodies). It runs browser.visibility with the
new features: [id or name] (and path: for one placement of a component
placed more than once), which hides or shows those bodies.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 22:49:23 +00:00
479 changed files with 47883 additions and 3309 deletions
+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).
+52 -8
View File
@@ -98,15 +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.
@@ -131,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') }}
@@ -140,6 +142,7 @@ jobs:
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
@@ -168,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
@@ -201,14 +227,20 @@ 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"
@@ -268,12 +300,24 @@ jobs:
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"
+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
+1 -1
View File
@@ -297,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
-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))
+65 -11
View File
@@ -128,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 |
@@ -171,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 |
@@ -292,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 |
@@ -381,6 +428,7 @@ 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/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 |
@@ -527,6 +575,12 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/scripts/hem.json | SolidCraft contributors | original parametric recipe for an 80 × 50 mm sheet with an 8 mm returned edge, authored for the command guide | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay.json | SolidCraft contributors | original SolidCraft theme file (Clay, light): warm paper palette chosen for SolidCraft | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay-dark.json | SolidCraft contributors | original SolidCraft theme file (Clay Dark): warm charcoal palette chosen for SolidCraft | MIT OR Apache-2.0 |
| 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 |
## Code adapted from sibling projects
+4
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
@@ -283,6 +284,9 @@ 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.
- **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
+27 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4217,6 +4217,7 @@ dependencies = [
"getrandom 0.3.4",
"hidapi",
"keyring",
"libc",
"log",
"muda",
"objc2 0.6.5",
@@ -4228,6 +4229,7 @@ dependencies = [
"rfd",
"ron",
"rustls",
"serde",
"serde_json",
"solidcraft-3dx-client",
"solidcraft-engine",
@@ -4240,11 +4242,11 @@ 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",
@@ -4256,7 +4258,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4265,7 +4267,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"log",
"serde",
@@ -4282,8 +4284,9 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"log",
"schemars",
"serde",
@@ -4302,7 +4305,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
"serde_json",
@@ -4311,7 +4314,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"log",
@@ -4331,7 +4334,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4355,7 +4358,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"log",
"proptest",
@@ -4375,7 +4378,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4384,7 +4387,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"image",
"moxcms",
@@ -4398,7 +4401,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
"serde_json",
@@ -4409,14 +4412,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"arboard",
"egui",
@@ -4438,6 +4441,7 @@ dependencies = [
"tiny-skia",
"toml",
"ttf-parser",
"unicode-bidi",
"web-time",
"winreg",
"zip",
@@ -4445,7 +4449,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"eframe",
"egui",
@@ -4947,6 +4951,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
@@ -5984,7 +5994,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.24.0"
version = "0.25.1"
dependencies = [
"proc-macro2",
"serde",
+1 -1
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"
+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.
+8 -2
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,6 +121,12 @@ 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
+48 -34
View File
@@ -1,6 +1,6 @@
Third-party software in SolidCraft
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 559 crates below.
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
@@ -439,6 +439,7 @@ typed-path 0.12.3 (MIT OR Apache-2.0)
typeid 1.0.3 (MIT OR Apache-2.0)
typenum 1.20.1 (MIT OR Apache-2.0)
uds_windows 1.2.1 (MIT)
unicode-bidi 0.3.18 (MIT OR Apache-2.0)
unicode-general-category 1.1.0 (Apache-2.0)
unicode-ident 1.0.26 ((MIT OR Apache-2.0) AND Unicode-3.0)
unicode-segmentation 1.13.3 (MIT OR Apache-2.0)
@@ -1123,9 +1124,9 @@ parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width
0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-bidi 0.3.18, unicode-segmentation
1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2
1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
wit-bindgen 0.57.1
================================================================================
@@ -14524,6 +14525,49 @@ MIT License
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
================================================================================
unicode-bidi 0.3.18
================================================================================
This project is copyright 2015, The Servo Project Developers (given in the
file AUTHORS).
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
http://opensource.org/licenses/MIT>, at your option. All files in the project
carrying such notice may not be copied, modified, or distributed except
according to those terms.
================================================================================
unicode-bidi 0.3.18, unicode-segmentation 1.13.3, unicode-width 0.2.2
================================================================================
Copyright (c) 2015 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
unicode-ident 1.0.26
================================================================================
@@ -14572,36 +14616,6 @@ authorization of the copyright holder.
unicode-segmentation 1.13.3, unicode-width 0.2.2
================================================================================
Copyright (c) 2015 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
unicode-segmentation 1.13.3, unicode-width 0.2.2
================================================================================
Licensed under the Apache License, Version 2.0
<LICENSE-APACHE or
http://www.apache.org/licenses/LICENSE-2.0> or the MIT
@@ -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
+5 -2
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.
@@ -205,7 +205,10 @@ pub fn run(args: &[String]) -> Result<(), String> {
);
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));
+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)) };
+16 -17
View File
@@ -1,10 +1,10 @@
//! 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]
//! solidcraft-cli eval <script.json|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]
@@ -56,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] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
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> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
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]\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]\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";
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.
@@ -83,7 +83,7 @@ const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
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.
@@ -392,7 +392,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
Ok(())
}
/// The session `run` and `snapshot` start from: `--in` (a design or STEP file) with the
/// 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> {
match (base, path) {
@@ -445,8 +445,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
let mut s = load(path)?;
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())?;
let out = json!({
@@ -818,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 };
@@ -864,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;
+3 -3
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);
}
@@ -1558,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}}]),
);
}
+7 -5
View File
@@ -139,11 +139,13 @@ fn eval_json_out_holds_what_stdout_would() {
fn json_out_needs_a_file_and_fails_when_it_cannot_write() {
let dir = Scratch::new();
let script = dir.script();
for cmd in ["run", "eval"] {
let output = cli().arg(cmd).arg(&script).arg("--json-out").output().unwrap();
assert_eq!(output.status.code(), Some(1), "{cmd}");
assert!(String::from_utf8_lossy(&output.stderr).contains("--json-out needs a FILE"), "{cmd}");
assert!(output.stdout.is_empty(), "{cmd}");
for cmd in ["run", "eval", "inspect"] {
for trailing in [&[][..], &["--in"][..]] {
let output = cli().arg(cmd).arg(&script).arg("--json-out").args(trailing).output().unwrap();
assert_eq!(output.status.code(), Some(1), "{cmd}");
assert!(String::from_utf8_lossy(&output.stderr).contains("--json-out needs a FILE"), "{cmd}");
assert!(output.stdout.is_empty(), "{cmd}");
}
let missing = dir.0.join("missing").join("out.json");
let output = cli().arg(cmd).arg(&script).arg("--json-out").arg(&missing).output().unwrap();
@@ -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();
@@ -150,7 +153,7 @@ fn pi_case_faces_per_step_at_both_heights() {
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() {
+110 -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();
@@ -267,6 +267,110 @@ fn results_with_base_design_and_quiet_still_report_and_save() {
assert_eq!(body_count(&base), 1);
}
/// `eval` and its alias must measure the script applied to the input design, rather than
/// silently reporting the input alone (#6861).
#[test]
fn eval_runs_its_script_on_the_in_design() {
let dir = Scratch::new();
let base = dir.0.join("base.solidcraft");
let script = dir.0.join("add.json");
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&base).output().unwrap());
let original = std::fs::read(&base).unwrap();
std::fs::write(
&script,
json!({"commands": [{"command": "solid.box", "params": {"length": 5, "width": 3, "height": 4, "corner": [10, 0, 0]}}]}).to_string(),
)
.unwrap();
for command in ["eval", "inspect"] {
// Both existing one-input forms still measure just the original body.
for input in [&base, &dir.script()] {
for named_input in [false, true] {
let mut c = cli();
c.arg(command);
if named_input {
c.arg("--in");
}
let measured = json_output(c.arg(input).output().unwrap());
assert_eq!(measured["body_count"], 1);
assert!((measured["total"]["volume_mm3"].as_f64().unwrap() - 24.0).abs() < 1e-6);
}
}
for input_first in [false, true] {
for to_file in [false, true] {
let mut c = cli();
c.arg(command);
if input_first {
c.arg("--in").arg(&base).arg(&script);
} else {
c.arg(&script).arg("--in").arg(&base);
}
let report = dir.0.join("measurement.json");
if to_file {
c.arg("--json-out").arg(&report);
}
let output = c.output().unwrap();
let measured = if to_file {
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert!(output.stdout.is_empty());
serde_json::from_slice(&std::fs::read(&report).unwrap()).unwrap()
} else {
json_output(output)
};
assert_eq!(measured["body_count"], 2, "{command}, input_first={input_first}, to_file={to_file}");
assert!((measured["total"]["volume_mm3"].as_f64().unwrap() - 84.0).abs() < 1e-6);
assert_eq!(measured["timeline"].as_array().unwrap().len(), 2, "the script runs exactly once");
assert_eq!(std::fs::read(&base).unwrap(), original, "evaluation never saves its changes to the input");
}
}
}
}
#[test]
fn eval_propagates_script_errors_before_writing_a_report() {
let dir = Scratch::new();
let base = dir.script();
let script = dir.0.join("bad.json");
for command in ["eval", "inspect"] {
for contents in ["{", r#"{"commands":[{"command":"missing.command","params":{}}]}"#] {
std::fs::write(&script, contents).unwrap();
let report = dir.0.join("report.json");
let output = cli().arg(command).arg(&script).arg("--in").arg(&base).arg("--json-out").arg(&report).output().unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(!output.stderr.is_empty());
assert!(output.stdout.is_empty());
assert!(!report.exists());
}
}
}
#[test]
fn eval_rejects_unknown_options_and_missing_input_values_before_writing_a_report() {
let dir = Scratch::new();
let script = dir.script();
let report = dir.0.join("report.json");
for command in ["eval", "inspect"] {
for (options, message) in [(&["--frobnicate", "1"][..], "doesn't take --frobnicate"), (&["--in"][..], "--in needs a value")] {
for options_first in [false, true] {
std::fs::write(&report, b"previous report").unwrap();
let mut c = cli();
c.arg(command);
if options_first {
c.args(options).arg("--json-out").arg(&report).arg(&script);
} else {
c.arg(&script).args(options).arg("--json-out").arg(&report);
}
// A following option also proves that --in cannot consume a flag as its value.
let output = c.output().unwrap();
assert_eq!(output.status.code(), Some(1), "{command}, {options:?}");
let error = String::from_utf8_lossy(&output.stderr);
assert!(error.contains(message), "{command}: {error}");
assert!(output.stdout.is_empty());
assert_eq!(std::fs::read(&report).unwrap(), b"previous report");
}
}
}
}
#[test]
fn results_propagate_output_errors() {
let dir = Scratch::new();
@@ -308,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,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 {
+1 -1
View File
@@ -37,5 +37,5 @@ web-sys = { version = "0.3", features = [
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "FormData", "RequestInit", "MediaQueryList", "Gamepad", "Performance",
] }
+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]
+40
View File
@@ -0,0 +1,40 @@
//! Open with EasyPrint in the browser: upload the model to Printables' import endpoint with
//! `fetch` (it allows any origin) and answer with the address to open.
use std::sync::mpsc::Sender;
use js_sys::{Array, Uint8Array};
use solidcraft_engine::easyprint::{IMPORT_URL, parse_import_response};
use wasm_bindgen::JsCast as _;
use wasm_bindgen_futures::{JsFuture, spawn_local};
use crate::files::err;
/// Start the upload; the answer arrives on `done`.
pub fn upload(name: &str, bytes: Vec<u8>, done: Sender<Result<String, String>>) {
let name = name.to_string();
spawn_local(async move {
let _ = done.send(post(&name, &bytes).await);
});
}
async fn post(name: &str, bytes: &[u8]) -> Result<String, String> {
let window = web_sys::window().ok_or("no window")?;
let parts = Array::new();
parts.push(&Uint8Array::from(bytes));
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts).map_err(err)?;
let form = web_sys::FormData::new().map_err(err)?;
form.append_with_blob_and_filename("file", &blob, name).map_err(err)?;
form.append_with_str("file_name", name).map_err(err)?;
let headers = web_sys::Headers::new().map_err(err)?;
headers.set("Accept", "application/json").map_err(err)?;
let init = web_sys::RequestInit::new();
init.set_method("POST");
init.set_body(&form);
init.set_headers(&headers);
let response = JsFuture::from(window.fetch_with_str_and_init(IMPORT_URL, &init)).await.map_err(err)?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "response cast failed".to_string())?;
let status = response.status();
let text = JsFuture::from(response.text().map_err(err)?).await.map_err(err)?;
parse_import_response(status, text.as_string().unwrap_or_default().as_bytes())
}
+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")?;
+3
View File
@@ -11,6 +11,8 @@ mod api;
#[cfg(any(target_arch = "wasm32", test))]
mod disk;
#[cfg(target_arch = "wasm32")]
mod easyprint;
#[cfg(target_arch = "wasm32")]
mod files;
#[cfg(target_arch = "wasm32")]
mod install;
@@ -777,6 +779,7 @@ mod web {
})),
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
download_design: Some(Box::new(files::download)),
easyprint_upload: Some(std::sync::Arc::new(crate::easyprint::upload)),
browser_files: Some(Box::new(move || shown.panel.set(true))),
install_app: Some(crate::install::service(&cc.egui_ctx)),
..Default::default()
+7 -2
View File
@@ -13,8 +13,9 @@ bundled-render-library = ["solidcraft-engine/bundled-render-library"]
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
# (Windows resize border, taskbar icon) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
# (macOS file-open events), src/native_crash.rs (Windows exception filter), src/native_crash_macos.rs
# (macOS crash signal handler) and src/native_frame.rs (Windows resize border, taskbar icon) can
# allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
[lints.rust]
unsafe_code = "deny"
@@ -31,6 +32,7 @@ solidcraft-ui-egui = { workspace = true }
solidcraft-space-mouse = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
egui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
rfd = { workspace = true }
@@ -88,6 +90,9 @@ objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
# Native crash capture (src/native_crash_macos.rs): the signal handler, the faulting module
# (already in the tree through many crates).
libc = "0.2"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
+69 -21
View File
@@ -187,6 +187,35 @@ fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
out.write_all(&line)
}
/// The `"token"` of a request line that is a JSON object, read without building the rest of it.
/// Until a connection shows the token, a line costs only its own bytes: parsed whole, a 16 MiB
/// line of `[0,0,…]` is hundreds of MB, and 16 such connections could exhaust memory before any
/// token was checked.
fn token_of(line: &str) -> Option<String> {
struct Token;
impl<'de> serde::de::Visitor<'de> for Token {
type Value = Option<String>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a request object")
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut token = None;
while let Some(key) = map.next_key::<String>()? {
if key == "token" {
token = Some(map.next_value::<String>()?);
} else {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
Ok(token)
}
}
let mut de = serde_json::Deserializer::from_str(line);
let token = serde::Deserializer::deserialize_map(&mut de, Token).ok()?;
de.end().ok()?;
token
}
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
@@ -388,7 +417,6 @@ impl Lines {
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
_counted: Subscriber,
}
impl Subscription {
@@ -406,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))
}
}
@@ -429,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() {
@@ -486,18 +519,18 @@ fn serve_with(
if line.trim().is_empty() {
continue;
}
// The first request must carry the token; later ones on the same connection may.
if !authenticated {
match token_of(&line) {
Some(t) if control_auth::tokens_match(&t, token) => authenticated = true,
_ => {
refuse(&mut out, &mut reader);
break;
}
}
}
let reply = match serde_json::from_str::<Value>(&line) {
Ok(msg) => {
// The first request must carry the token; later ones on the same connection may.
if !authenticated {
match msg.get("token").and_then(Value::as_str) {
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
_ => {
refuse(&mut out, &mut reader);
break;
}
}
}
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
let params = msg.get("params").cloned().unwrap_or(json!({}));
@@ -563,10 +596,6 @@ fn serve_with(
}
r
}
Err(_) if !authenticated => {
refuse(&mut out, &mut reader);
break;
}
Err(e) => {
bad += 1;
json!({"ok": false, "error": format!("bad JSON: {e}")})
@@ -841,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());
}
@@ -869,6 +904,19 @@ mod tests {
assert!(render.contains("will still write the file"), "{render}");
}
/// The token is read from a request object without building the rest of it: other fields,
/// however large or nested, are only skipped. Anything but an object with a string token is
/// no token.
#[test]
fn token_is_read_without_parsing_the_request() {
assert_eq!(token_of(r#"{"id":1,"token":"abc","params":{"a":[1,[2,{"b":null}]]}}"#).as_deref(), Some("abc"));
let flood = format!(r#"{{"params":[{}0],"token":"t"}}"#, "0,".repeat(1 << 20));
assert_eq!(token_of(&flood).as_deref(), Some("t"));
for no in [r#"["abc"]"#, r#"{"token":5}"#, r#"{"token":"a"} x"#, r#"{"id":1}"#, "GET / HTTP/1.1", r#""token""#] {
assert_eq!(token_of(no), None, "{no}");
}
}
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
/// need not be repeated on that connection.
+155 -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.
@@ -24,6 +24,8 @@ mod control_server;
mod device_lost;
#[cfg(target_os = "windows")]
mod native_crash;
#[cfg(target_os = "macos")]
mod native_crash_macos;
#[cfg(target_os = "windows")]
mod native_frame;
#[cfg(target_os = "macos")]
@@ -90,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 {
@@ -120,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);
@@ -144,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);
@@ -238,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> {
@@ -287,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));
@@ -308,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).
@@ -323,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");
@@ -333,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])
@@ -344,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()),
@@ -356,6 +420,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
can_restore: None,
restore_file: None,
download_design: None,
easyprint_upload: Some(std::sync::Arc::new(easyprint_upload)),
browser_files: None,
agent_http: Some(std::sync::Arc::new(agent_http)),
secrets: Some(std::sync::Arc::new(Keychain)),
@@ -452,6 +517,24 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
Ok((status, body))
}
/// Open with EasyPrint: upload the model to Printables' import endpoint on a worker and answer on
/// `done` with the address to open. Through [`agent_http`]: no redirects, a timeout, no cookies.
fn easyprint_upload(name: &str, bytes: Vec<u8>, done: std::sync::mpsc::Sender<Result<String, String>>) {
use solidcraft_engine::easyprint;
let name = name.to_string();
std::thread::spawn(move || {
let (content_type, body) = easyprint::multipart(&name, &bytes);
let request = solidcraft_ui_egui::agent_chat::backend::HttpRequest {
method: "POST",
url: easyprint::IMPORT_URL.into(),
headers: vec![("Content-Type".into(), content_type), ("Accept".into(), "application/json".into())],
body: Some(body),
timeout_s: 180,
};
let _ = done.send(agent_http(&request).and_then(|(status, body)| easyprint::parse_import_response(status, &body)));
});
}
/// API keys in the system keychain (service "SolidCraft agent chat", one entry per agent).
struct Keychain;
@@ -532,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)?;
@@ -667,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"));
@@ -691,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),
@@ -738,10 +826,12 @@ fn main() -> eframe::Result {
solidcraft_engine::crash::install(dir, build)
})
.unwrap_or_default();
// A crash in native code (a graphics driver) says where it happened: the exception and the
// module on Windows, and on every system the startup step the app was in.
// A crash in native code (a graphics driver) says where it happened: the exception or signal
// and the module on Windows and macOS, and on every system the startup step the app was in.
#[cfg(target_os = "windows")]
native_crash::install();
#[cfg(target_os = "macos")]
native_crash_macos::install();
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
let started = match &wgpu_options.wgpu_setup {
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
@@ -817,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")]
@@ -840,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(),
@@ -910,6 +1006,10 @@ fn main() -> eframe::Result {
open_documents::set_ui(&cc.egui_ctx);
[files, open_documents::take()].concat()
};
// No design given: the designs open at the last quit, when the preference asks (#5542).
// Not when driven over the control channel, whose clients expect a fresh design.
let files =
if files.is_empty() && !sample && control_port.is_none() { solidcraft_ui_egui::documents::designs_to_reopen(&app) } else { files };
for f in &files {
app.open_path_async(f);
}
@@ -966,6 +1066,7 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
native_menu,
nav_bridge,
dialog_folder,
}))
}),
);
@@ -1144,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")]
@@ -1253,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);
+879
View File
@@ -0,0 +1,879 @@
//! macOS: what a crash in native code leaves behind (docs/crash-reports.md).
//!
//! A crash inside a graphics driver or another library (a segfault, an `abort()`, a stack
//! overflow) is no Rust panic: the panic hook never runs and the next start only found "an
//! unclean exit". A signal handler for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP (how a trap
//! instruction arrives on Apple silicon) and SIGABRT now writes, before the process goes, the
//! same small JSON file as the Windows filter (`native_crash.rs`): the signal, the faulting
//! module and the offset into it, and a short frame-pointer walk, next to the run's crash journal
//! (`solidcraft_engine::crash::native_paths`). The next start puts it into the unclean exit's
//! report. The signal then goes on to the handler that was there before (the standard library's
//! stack overflow message) or to the default action, so the process still dies of it and macOS
//! still writes its own crash report (`~/Library/Logs/DiagnosticReports`; the file names the
//! `pid` the JSON records).
//!
//! Installing a signal handler and asking dyld which image holds an address have no safe API,
//! hence the `unsafe_code` allowance on this module (the app crate denies it). The handler runs
//! on a thread that may have a broken heap, or on the small alternate stack after a stack
//! overflow: it doesn't allocate, formats into a buffer on the stack, and only reads memory on
//! the thread's own stack. It writes what it knows from the signal first and only then calls
//! `dladdr` (not on POSIX's async-signal-safe list; it takes dyld's recursive reader lock and
//! doesn't allocate) to name the modules, rewriting the file, so a hang or a fault in there
//! still leaves a record.
#![allow(unsafe_code)]
use std::ffi::{CStr, CString, c_int, c_void};
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
use libc::{SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP, siginfo_t};
/// The signals a crash arrives as, in the order of [`PREVIOUS`].
const SIGNALS: [c_int; 6] = [SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP, SIGABRT];
/// Frames the walk records (the faulting instruction first).
const MAX_FRAMES: usize = 24;
/// A fault this far below a thread's stack is taken for a stack overflow.
const GUARD: usize = 64 << 10;
/// Where the handler writes (NUL-terminated), set once by [`install`].
static PATH: OnceLock<CString> = OnceLock::new();
/// The handler that was installed before ours for each of [`SIGNALS`] (called after ours), as an
/// address (`SIG_DFL` is 0, `SIG_IGN` 1), and its `sa_flags`.
static PREVIOUS: [AtomicUsize; 6] = [const { AtomicUsize::new(libc::SIG_DFL) }; 6];
static PREVIOUS_FLAGS: [AtomicI32; 6] = [const { AtomicI32::new(0) }; 6];
/// The handler has started writing (a second signal while it does is only passed on).
static BUSY: AtomicBool = AtomicBool::new(false);
/// Catch native crashes from now on (after `solidcraft_engine::crash::install`; nothing without it).
pub fn install() {
let Some((info, _)) = solidcraft_engine::crash::native_paths() else { return };
let Ok(path) = CString::new(info.as_os_str().as_bytes()) else { return };
if PATH.set(path).is_err() {
return;
}
for (slot, &signal) in SIGNALS.iter().enumerate() {
// SAFETY: `sigaction` is plain data; all zeros is a valid (empty) value.
let mut ours: libc::sigaction = unsafe { std::mem::zeroed() };
ours.sa_sigaction = handler as *const () as usize;
// On the alternate stack the standard library gives each of its threads, so that a stack
// overflow can still be recorded.
ours.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
// SAFETY: as above.
let mut old: libc::sigaction = unsafe { std::mem::zeroed() };
// SAFETY: `handler` has the signature SA_SIGINFO calls it with and stays valid for the
// process's life; both structs are valid for the call.
if unsafe { libc::sigaction(signal, &ours, &mut old) } == 0
&& old.sa_sigaction != ours.sa_sigaction
&& let (Some(p), Some(f)) = (PREVIOUS.get(slot), PREVIOUS_FLAGS.get(slot))
{
f.store(old.sa_flags, Ordering::SeqCst);
p.store(old.sa_sigaction, Ordering::SeqCst);
}
}
}
/// The signal handler: record, then pass the signal on.
unsafe extern "C" fn handler(signal: c_int, info: *mut siginfo_t, context: *mut c_void) {
// SAFETY: `__error` is this thread's errno, restored for whatever runs after us.
let errno = unsafe { *libc::__error() };
let slot = SIGNALS.iter().position(|&s| s == signal).unwrap_or(0);
let previous = PREVIOUS.get(slot).map_or(libc::SIG_DFL, |p| p.load(Ordering::SeqCst));
// SAFETY: the system passes a valid siginfo and the interrupted thread's context for the
// duration of an SA_SIGINFO handler.
let f = unsafe { fault(signal, info, context) };
// Someone sent a signal the app ignored before: still nothing to record.
let ignored = previous == libc::SIG_IGN && !f.fault;
if !ignored && !BUSY.swap(true, Ordering::SeqCst) {
record(&f);
}
// SAFETY: as above.
unsafe { pass_on(signal, slot, previous, f.fault, info, context) };
// SAFETY: as above.
unsafe { *libc::__error() = errno };
}
/// The signal comes from the faulting instruction itself (not `kill`, `raise` or `abort`), so
/// returning runs it again: on arm64, the exception the thread last took matches the signal.
/// macOS gives a SIGSEGV sent with `kill` the same `si_code` as a real one (SEGV_ACCERR), but
/// the exception is then the thread's last system call (or something long past, whose fault
/// address doesn't match).
#[cfg(any(target_arch = "aarch64", test))]
fn arm64_fault(signal: c_int, si_code: c_int, addr: usize, esr: u32, far: u64) -> bool {
const ABORTS: [u32; 4] = [0x20, 0x21, 0x24, 0x25];
const SVC: u32 = 0x15;
let class = esr >> 26;
signal != SIGABRT
&& (1..0x10000).contains(&si_code)
&& match signal {
SIGSEGV | SIGBUS => ABORTS.contains(&class) && far as usize == addr,
_ => class != SVC && !ABORTS.contains(&class),
}
}
/// As [`arm64_fault`], from the x86 trap number and page-fault address.
#[cfg(any(target_arch = "x86_64", test))]
fn x86_fault(signal: c_int, si_code: c_int, addr: usize, trapno: u16, faultvaddr: u64) -> bool {
signal != SIGABRT
&& (1..0x10000).contains(&si_code)
&& match signal {
SIGSEGV | SIGBUS => (trapno == 14 && faultvaddr as usize == addr) || matches!(trapno, 12 | 13),
SIGILL => trapno == 6,
SIGFPE => matches!(trapno, 0 | 16 | 19),
SIGTRAP => matches!(trapno, 1 | 3),
_ => false,
}
}
/// Hand the signal to the previous handler, or to the default action. A crash signal that was
/// only sent goes to the default action: the handler before ours (the standard library's, for
/// SIGSEGV and SIGBUS) expects a fault that happens again when it returns.
unsafe fn pass_on(signal: c_int, slot: usize, previous: usize, fault: bool, info: *mut siginfo_t, context: *mut c_void) {
let ours = handler as *const () as usize;
if previous != libc::SIG_DFL && previous != libc::SIG_IGN && previous != ours && (fault || signal == SIGABRT) {
let flags = PREVIOUS_FLAGS.get(slot).map_or(0, |f| f.load(Ordering::SeqCst));
if flags & libc::SA_SIGINFO != 0 {
// SAFETY: the address came from `sigaction` as an SA_SIGINFO handler.
let previous: unsafe extern "C" fn(c_int, *mut siginfo_t, *mut c_void) = unsafe { std::mem::transmute(previous) };
// SAFETY: as for this handler.
unsafe { previous(signal, info, context) };
} else {
// SAFETY: the address came from `sigaction` as a plain handler.
let previous: unsafe extern "C" fn(c_int) = unsafe { std::mem::transmute(previous) };
// SAFETY: as for this handler.
unsafe { previous(signal) };
}
return;
}
if previous == libc::SIG_IGN && !fault {
return;
}
// SAFETY: as in `install`.
let mut default: libc::sigaction = unsafe { std::mem::zeroed() };
default.sa_sigaction = libc::SIG_DFL;
// SAFETY: a valid struct; restoring the default action is async-signal-safe.
unsafe { libc::sigaction(signal, &default, std::ptr::null_mut()) };
// A fault happens again when we return, now with the default action, so macOS reports it at
// the instruction; anything else is raised again, and arrives once this handler returns.
if !fault {
// SAFETY: async-signal-safe.
unsafe { libc::raise(signal) };
}
}
/// The darwin `ucontext_t`, up to the pointer to the machine context (`<sys/_types/_ucontext.h>`).
#[repr(C)]
struct UContext {
_onstack: c_int,
_sigmask: u32,
_stack: libc::stack_t,
_link: *mut c_void,
_mcsize: usize,
mcontext: *const MContext,
}
/// The darwin arm64 machine context: exception state, then the general registers
/// (`<mach/arm/_structs.h>`; the floating-point state that follows isn't read).
#[cfg(target_arch = "aarch64")]
#[repr(C)]
struct MContext {
far: u64,
esr: u32,
_exception: u32,
_x: [u64; 29],
fp: u64,
_lr: u64,
_sp: u64,
pc: u64,
}
/// The darwin x86_64 machine context: exception state, then the general registers
/// (`<mach/i386/_structs.h>`; rax rbx rcx rdx rdi rsi rbp rsp r8–r15, then rip).
#[cfg(target_arch = "x86_64")]
#[repr(C)]
struct MContext {
trapno: u16,
_cpu: u16,
err: u32,
faultvaddr: u64,
gpr: [u64; 16],
rip: u64,
}
/// What the faulting instruction did to its address, when the processor says.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Access {
Unknown,
Read,
Write,
Execute,
}
/// What a crash signal says, read out of the signal before anything is looked up.
#[derive(Clone, Copy, Debug)]
struct Fault {
signal: c_int,
si_code: c_int,
/// `si_addr`: the address that faulted (for SIGILL, SIGFPE and SIGTRAP, the instruction).
addr: usize,
pc: usize,
fp: usize,
access: Access,
/// It comes from the faulting instruction (not `kill`, `raise` or `abort`).
fault: bool,
/// The process that sent the signal (0 or our own for a fault or an `abort()`).
sender: i32,
pid: i32,
thread: u64,
thread_name: [u8; 64],
/// The thread's stack, `[low, high)` (0, 0 when unknown).
stack: (usize, usize),
}
impl Fault {
/// The fault hit just below the thread's stack: the stack ran out.
fn overflow(&self) -> bool {
let (low, high) = self.stack;
matches!(self.signal, SIGSEGV | SIGBUS) && high > low && self.addr < low.saturating_add(4096) && self.addr >= low.saturating_sub(GUARD)
}
fn signal_name(&self) -> &'static str {
match self.signal {
SIGSEGV => "SIGSEGV",
SIGBUS => "SIGBUS",
SIGILL => "SIGILL",
SIGFPE => "SIGFPE",
SIGTRAP => "SIGTRAP",
SIGABRT => "SIGABRT",
_ => "signal",
}
}
fn name(&self) -> &'static str {
if self.overflow() {
return "stack overflow";
}
match self.signal {
SIGSEGV => "segmentation fault",
SIGBUS => "bus error",
SIGILL => "illegal instruction",
SIGFPE => "arithmetic exception",
SIGTRAP => "trap",
SIGABRT => "abort",
_ => "signal",
}
}
/// The `si_code` names that say something about a crash.
fn code_name(&self) -> Option<&'static str> {
Some(match (self.signal, self.si_code) {
(SIGSEGV, 1) => "SEGV_MAPERR",
(SIGSEGV, 2) => "SEGV_ACCERR",
(SIGBUS, 1) => "BUS_ADRALN",
(SIGBUS, 2) => "BUS_ADRERR",
(SIGBUS, 3) => "BUS_OBJERR",
(SIGFPE, 7) => "FPE_INTDIV",
(SIGFPE, 8) => "FPE_INTOVF",
_ => return None,
})
}
/// One line on what happened: the address read or written, a stack overflow, who sent it.
fn detail(&self, b: &mut Buf) {
if self.sender != 0 && self.sender != self.pid {
let _ = write!(b, "sent by process {}", self.sender);
return;
}
if !self.fault {
if self.signal != SIGABRT {
let _ = b.write_str("sent with kill or raise, not a fault");
}
return;
}
let what = match self.access {
Access::Read => "read of",
Access::Write => "write to",
Access::Execute => "execution at",
Access::Unknown => "access to",
};
if self.overflow() {
let _ = write!(b, "{what} 0x{:016X}, below the thread's stack", self.addr);
} else if matches!(self.signal, SIGSEGV | SIGBUS) {
let _ = write!(b, "{what} 0x{:016X}", self.addr);
} else {
let _ = write!(b, "at 0x{:016X}", self.addr);
}
if let Some(c) = self.code_name() {
let _ = write!(b, " ({c})");
}
}
}
/// An image that holds an address, as `dladdr` names it.
struct Module<'a> {
path: &'a [u8],
base: usize,
symbol: Option<(&'a [u8], usize)>,
}
/// A fixed buffer to format into without allocating (what doesn't fit is cut off).
struct Buf {
bytes: [u8; 8192],
len: usize,
}
impl Buf {
fn new() -> Buf {
Buf { bytes: [0; 8192], len: 0 }
}
fn room(&self) -> usize {
self.bytes.len().saturating_sub(self.len)
}
fn as_bytes(&self) -> &[u8] {
self.bytes.get(..self.len).unwrap_or_default()
}
/// `s` as the inside of a JSON string, at most `max` bytes of it (bytes that aren't UTF-8
/// and control characters become `?`).
fn escaped(&mut self, s: &[u8], max: usize) {
for chunk in s.get(..s.len().min(max)).unwrap_or_default().utf8_chunks() {
for c in chunk.valid().chars() {
let _ = match c {
'"' => self.write_str("\\\""),
'\\' => self.write_str("\\\\"),
c if (c as u32) < 0x20 => self.write_str("?"),
c => self.write_char(c),
};
}
if !chunk.invalid().is_empty() {
let _ = self.write_str("?");
}
}
}
}
impl std::fmt::Write for Buf {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let n = s.len().min(self.room());
if let (Some(to), Some(from)) = (self.bytes.get_mut(self.len..self.len + n), s.as_bytes().get(..n)) {
to.copy_from_slice(from);
self.len += n;
}
Ok(())
}
}
/// The last part of a path.
fn file_name(path: &[u8]) -> &[u8] {
path.rsplit(|&c| c == b'/').next().unwrap_or(path)
}
/// The report: what the signal says, then (with `resolve`) the module of the faulting
/// instruction and the `frames`. Always valid JSON: frames stop while there is room to close it.
fn json(b: &mut Buf, f: &Fault, frames: &[usize], resolve: impl Fn(usize, bool) -> Option<Module<'static>>) {
let signal = f.signal_name();
let _ = write!(b, "{{\"signal\":\"{signal}\",\"number\":{},\"code\":\"{signal}\",\"name\":\"{}\"", f.signal, f.name());
let _ = write!(b, ",\"si_code\":{},\"address\":\"0x{:016X}\",\"thread\":{},\"pid\":{}", f.si_code, f.pc, f.thread, f.pid);
let name = f.thread_name.split(|&c| c == 0).next().unwrap_or_default();
if !name.is_empty() {
let _ = b.write_str(",\"thread_name\":\"");
b.escaped(name, 64);
let _ = b.write_str("\"");
}
let mark = b.len;
let _ = b.write_str(",\"detail\":\"");
let start = b.len;
f.detail(b);
if b.len == start {
b.len = mark;
} else {
let _ = b.write_str("\"");
}
if let Some(m) = resolve(f.pc, false) {
let _ = write!(b, ",\"offset\":\"0x{:X}\",\"module\":\"", f.pc.wrapping_sub(m.base));
b.escaped(m.path, 300);
let _ = b.write_str("\"");
}
if !frames.is_empty() {
let _ = b.write_str(",\"frames\":[");
for (i, &at) in frames.iter().enumerate() {
// A frame is at most ~300 bytes; keep room for it and the closing `]}`.
if b.room() < 400 {
break;
}
if i > 0 {
let _ = b.write_str(",");
}
let _ = b.write_str("\"");
// A return address points after the call: look up the call itself.
match resolve(at, i > 0) {
Some(m) => {
b.escaped(file_name(m.path), 100);
let _ = write!(b, "+0x{:X}", at.wrapping_sub(m.base));
if let Some((symbol, start)) = m.symbol {
let _ = b.write_str(" ");
b.escaped(symbol, 160);
let _ = write!(b, "+0x{:X}", at.wrapping_sub(start));
}
}
None => {
let _ = write!(b, "0x{at:016X}");
}
}
let _ = b.write_str("\"");
}
let _ = b.write_str("]");
}
let _ = b.write_str("}");
}
/// The return addresses up the frame-pointer chain from `fp`, after `pc`. `read` gives the word
/// at an address; frames outside `stack`, misaligned or not going up end the walk.
fn walk(pc: usize, fp: usize, stack: (usize, usize), read: impl Fn(usize) -> Option<usize>, out: &mut [usize; MAX_FRAMES]) -> usize {
let (low, high) = stack;
let mut n = 0;
if let Some(slot) = out.get_mut(n) {
*slot = pc;
n += 1;
}
let mut fp = fp;
while n < MAX_FRAMES {
let in_stack = fp >= low && fp.checked_add(16).is_some_and(|end| end <= high);
if !fp.is_multiple_of(8) || !in_stack {
break;
}
let (Some(next), Some(ret)) = (read(fp), read(fp + 8)) else { break };
let ret = strip_pointer_auth(ret);
if ret == 0 {
break;
}
if let Some(slot) = out.get_mut(n) {
*slot = ret;
n += 1;
}
if next <= fp {
break;
}
fp = next;
}
n
}
/// Return addresses saved by arm64e code carry a signature in their top bits.
fn strip_pointer_auth(addr: usize) -> usize {
if cfg!(target_arch = "aarch64") { addr & 0x0000_7FFF_FFFF_FFFF } else { addr }
}
/// Read what the signal and the interrupted thread's registers say.
unsafe fn fault(signal: c_int, info: *const siginfo_t, context: *const c_void) -> Fault {
let mut f = Fault {
signal,
si_code: 0,
addr: 0,
pc: 0,
fp: 0,
access: Access::Unknown,
fault: false,
sender: 0,
// SAFETY: async-signal-safe.
pid: unsafe { libc::getpid() },
thread: 0,
thread_name: [0; 64],
stack: (0, 0),
};
if !info.is_null() {
// SAFETY: the system passes a valid siginfo for the duration of the handler.
let info = unsafe { &*info };
f.si_code = info.si_code;
f.addr = info.si_addr as usize;
f.sender = info.si_pid;
// Without registers (never seen), the signal's own code has to do.
f.fault = signal != SIGABRT && (1..0x10000).contains(&f.si_code);
}
let context = context as *const UContext;
// SAFETY: an SA_SIGINFO handler's third argument is the interrupted thread's ucontext, whose
// machine context pointer the kernel fills in.
let mcontext = if context.is_null() { std::ptr::null() } else { unsafe { (*context).mcontext } };
if !mcontext.is_null() {
// SAFETY: as above.
let m = unsafe { &*mcontext };
#[cfg(target_arch = "aarch64")]
{
f.pc = strip_pointer_auth(m.pc as usize);
f.fp = m.fp as usize;
f.fault = arm64_fault(signal, f.si_code, f.addr, m.esr, m.far);
// A data abort says whether it was a write (WnR); an instruction abort is execution.
f.access = match m.esr >> 26 {
0x24 | 0x25 if m.esr & (1 << 6) != 0 => Access::Write,
0x24 | 0x25 => Access::Read,
0x20 | 0x21 => Access::Execute,
_ => Access::Unknown,
};
}
#[cfg(target_arch = "x86_64")]
{
f.pc = m.rip as usize;
f.fp = m.gpr.get(6).copied().unwrap_or(0) as usize;
f.fault = x86_fault(signal, f.si_code, f.addr, m.trapno, m.faultvaddr);
// A page fault's error code: bit 4 instruction fetch, bit 1 write.
f.access = match (m.trapno, m.err) {
(14, e) if e & 0x10 != 0 => Access::Execute,
(14, e) if e & 0x2 != 0 => Access::Write,
(14, _) => Access::Read,
_ => Access::Unknown,
};
}
}
if f.pc == 0 && matches!(signal, SIGILL | SIGFPE | SIGTRAP) {
f.pc = f.addr;
}
// SAFETY: these read the calling thread's own pthread structure; the buffers are as long as
// we say.
unsafe {
let me = libc::pthread_self();
libc::pthread_threadid_np(me, &mut f.thread);
libc::pthread_getname_np(me, f.thread_name.as_mut_ptr().cast(), f.thread_name.len());
let high = libc::pthread_get_stackaddr_np(me) as usize;
let size = libc::pthread_get_stacksize_np(me);
if let Some(low) = high.checked_sub(size) {
f.stack = (low, high);
}
}
f
}
/// The image that holds `addr` (the call before it, for a return address).
fn resolve(addr: usize, return_address: bool) -> Option<Module<'static>> {
let at = if return_address { addr.saturating_sub(1) } else { addr };
// SAFETY: `Dl_info` is plain data; all zeros is a valid value.
let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
// SAFETY: `dladdr` only reads dyld's image list; the strings it returns live as long as the
// image, which a crashing process never unloads.
unsafe {
if libc::dladdr(at as *const c_void, &mut info) == 0 || info.dli_fname.is_null() {
return None;
}
let symbol = (!info.dli_sname.is_null()).then(|| (CStr::from_ptr(info.dli_sname).to_bytes(), info.dli_saddr as usize));
Some(Module { path: CStr::from_ptr(info.dli_fname).to_bytes(), base: info.dli_fbase as usize, symbol })
}
}
/// Write what the signal says into the info file, then again with the modules and frames.
fn record(f: &Fault) {
let Some(path) = PATH.get() else { return };
// SAFETY: NUL-terminated path; the descriptor is closed below.
let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_CLOEXEC, 0o600) };
if fd < 0 {
return;
}
let mut b = Buf::new();
json(&mut b, f, &[], |_, _| None);
let bytes = b.as_bytes();
// SAFETY: the buffer is as long as we say.
unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) };
let mut frames = [0usize; MAX_FRAMES];
// SAFETY: `walk` only reads inside the thread's own stack, 8-byte aligned.
let n = walk(f.pc, f.fp, f.stack, |at| Some(unsafe { (at as *const usize).read() }), &mut frames);
b.len = 0;
json(&mut b, f, frames.get(..n).unwrap_or_default(), resolve);
let bytes = b.as_bytes();
// SAFETY: as above.
unsafe {
if libc::pwrite(fd, bytes.as_ptr().cast(), bytes.len(), 0) == bytes.len() as isize {
libc::ftruncate(fd, bytes.len() as libc::off_t);
}
libc::close(fd);
}
}
#[cfg(test)]
mod tests {
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use super::*;
fn fault(signal: c_int, si_code: c_int, addr: usize) -> Fault {
let mut thread_name = [0u8; 64];
thread_name[..4].copy_from_slice(b"main");
Fault {
signal,
si_code,
addr,
pc: 0x1_0000_1234,
fp: 0,
access: Access::Read,
fault: true,
sender: 0,
pid: 77,
thread: 4242,
thread_name,
stack: (0x7000_0000, 0x7080_0000),
}
}
fn report(f: &Fault, frames: &[usize]) -> serde_json::Value {
let mut b = Buf::new();
json(&mut b, f, frames, |at, _| {
(at >= 0x1_0000_0000).then_some(Module {
path: b"/Applications/SolidCraft.app/Contents/MacOS/solidcraft",
base: 0x1_0000_0000,
symbol: Some((b"_ZN4main17h0123456789abcdefE", 0x1_0000_1200)),
})
});
serde_json::from_slice(b.as_bytes()).unwrap()
}
#[test]
fn a_segfault_says_the_signal_the_address_read_and_the_module() {
let v = report(&fault(SIGSEGV, 1, 0x10), &[0x1_0000_1234, 0x1_0000_2000, 0x5000]);
assert_eq!(v["signal"], "SIGSEGV");
assert_eq!(v["code"], "SIGSEGV");
assert_eq!(v["name"], "segmentation fault");
assert_eq!(v["detail"], "read of 0x0000000000000010 (SEGV_MAPERR)");
assert_eq!(v["address"], "0x0000000100001234");
assert_eq!(v["module"], "/Applications/SolidCraft.app/Contents/MacOS/solidcraft");
assert_eq!(v["offset"], "0x1234");
assert_eq!(v["thread"], 4242);
assert_eq!(v["thread_name"], "main");
assert_eq!(v["pid"], 77);
let frames: Vec<&str> = v["frames"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect();
assert_eq!(
frames,
["solidcraft+0x1234 _ZN4main17h0123456789abcdefE+0x34", "solidcraft+0x2000 _ZN4main17h0123456789abcdefE+0xE00", "0x0000000000005000"]
);
}
#[test]
fn a_fault_below_the_stack_is_a_stack_overflow() {
let mut f = fault(SIGBUS, 2, 0x7000_0000 - 0x100);
f.access = Access::Write;
let v = report(&f, &[]);
assert_eq!(v["name"], "stack overflow");
assert_eq!(v["code"], "SIGBUS");
assert_eq!(v["detail"], "write to 0x000000006FFFFF00, below the thread's stack (BUS_ADRERR)");
// Far below it, or inside it, it is not.
assert_eq!(report(&fault(SIGBUS, 2, 0x10), &[])["name"], "bus error");
assert_eq!(report(&fault(SIGSEGV, 2, 0x7040_0000), &[])["name"], "segmentation fault");
// Nor without knowing the stack.
let mut f = fault(SIGSEGV, 1, 0x7000_0000 - 0x100);
f.stack = (0, 0);
assert_eq!(report(&f, &[])["name"], "segmentation fault");
}
#[test]
fn an_abort_or_a_signal_sent_by_another_process_has_no_address_detail() {
let mut f = fault(SIGABRT, 0, 0);
f.fault = false;
f.sender = 77;
let v = report(&f, &[]);
assert_eq!(v["signal"], "SIGABRT");
assert_eq!(v["name"], "abort");
assert!(v.get("detail").is_none(), "{v}");
let mut f = fault(SIGSEGV, 0x10001, 0);
f.fault = false;
f.sender = 501;
assert_eq!(report(&f, &[])["detail"], "sent by process 501");
// `kill` without a sender, or `raise`.
f.sender = 0;
assert_eq!(report(&f, &[])["detail"], "sent with kill or raise, not a fault");
// A trap names the instruction.
assert_eq!(report(&fault(SIGTRAP, 1, 0x1_0000_1234), &[])["detail"], "at 0x0000000100001234");
}
#[test]
fn only_a_fault_runs_again_when_the_handler_returns() {
// Measured on an M5 Pro, macOS 27: a read of address 16, then `kill -SEGV` while the
// thread waits in a system call. Both have si_code 2.
assert!(arm64_fault(SIGSEGV, 2, 0x10, 0x9200_0046, 0x10));
assert!(!arm64_fault(SIGSEGV, 2, 0, 0x5600_0080, 0));
assert!(!arm64_fault(SIGBUS, 1, 0, 0x5600_0080, 0));
// A data abort long past, while the signal names no address.
assert!(!arm64_fault(SIGSEGV, 2, 0, 0x9200_0046, 0x10));
// A trap instruction (BRK) or an undefined one; but not a system call.
assert!(arm64_fault(SIGTRAP, 1, 0x1000, 0xF200_0001, 0));
assert!(arm64_fault(SIGILL, 1, 0x1000, 0x0200_0000, 0));
assert!(!arm64_fault(SIGILL, 1, 0x1000, 0x5600_0080, 0));
// Sent: SI_USER, no code, or an abort.
assert!(!arm64_fault(SIGSEGV, 0x10001, 0x10, 0x9200_0046, 0x10));
assert!(!arm64_fault(SIGTRAP, 0, 0, 0xF200_0001, 0));
assert!(!arm64_fault(SIGABRT, 1, 0, 0x5600_0080, 0));
// x86_64: a page fault at its address, a general protection fault, ud2, int3.
assert!(x86_fault(SIGSEGV, 1, 0x10, 14, 0x10));
assert!(!x86_fault(SIGSEGV, 1, 0, 14, 0x10));
assert!(x86_fault(SIGSEGV, 1, 0, 13, 0));
assert!(x86_fault(SIGILL, 1, 0x1000, 6, 0));
assert!(x86_fault(SIGTRAP, 1, 0x1000, 3, 0));
assert!(!x86_fault(SIGILL, 1, 0x1000, 14, 0));
}
#[test]
fn names_with_quotes_control_characters_or_bad_utf8_stay_valid_json() {
let mut f = fault(SIGILL, 1, 0x1_0000_1234);
f.thread_name = [0; 64];
f.thread_name[..7].copy_from_slice(b"a\"b\\\n\xFFc");
let v = report(&f, &[]);
assert_eq!(v["thread_name"], "a\"b\\??c");
}
#[test]
fn a_report_with_long_names_still_closes_its_json() {
let long: &'static [u8] = Box::leak(vec![b'x'; 4000].into_boxed_slice());
let f = fault(SIGSEGV, 1, 0x10);
let mut b = Buf::new();
json(&mut b, &f, &[0x1_0000_1234; MAX_FRAMES], |_, _| Some(Module { path: long, base: 0x1_0000_0000, symbol: Some((long, 0x1_0000_0000)) }));
let v: serde_json::Value = serde_json::from_slice(b.as_bytes()).unwrap();
assert_eq!(v["module"].as_str().unwrap().len(), 300);
let frames = v["frames"].as_array().unwrap();
assert!(!frames.is_empty() && frames.len() <= MAX_FRAMES, "{}", frames.len());
}
#[test]
fn the_frame_walk_stays_on_the_stack_and_goes_up() {
let stack = (0x1000, 0x2000);
// Three frames: 0x1100 -> 0x1200 -> 0x1300 -> 0 (end).
let memory = |at: usize| match at {
0x1100 => Some(0x1200),
0x1108 => Some(0xA),
0x1200 => Some(0x1300),
0x1208 => Some(0xB),
0x1300 => Some(0),
0x1308 => Some(0xC),
_ => None,
};
let mut out = [0; MAX_FRAMES];
let n = walk(0x9, 0x1100, stack, memory, &mut out);
assert_eq!(&out[..n], &[0x9, 0xA, 0xB, 0xC]);
// A frame pointer off the stack, misaligned, or one that doesn't go up ends the walk.
assert_eq!(walk(0x9, 0x3000, stack, memory, &mut out), 1);
assert_eq!(walk(0x9, 0x1101, stack, memory, &mut out), 1);
let looping = |at: usize| if at.is_multiple_of(16) { Some(at) } else { Some(0xA) };
assert_eq!(walk(0x9, 0x1100, stack, looping, &mut out), 2);
// A chain that never ends stops at MAX_FRAMES.
let endless = |at: usize| if at.is_multiple_of(16) { Some(at + 16) } else { Some(0xA) };
assert_eq!(walk(0x9, 0x1000, stack, endless, &mut out), MAX_FRAMES);
}
/// The child half: install crash reporting and the handler, then crash the way
/// `SOLIDCRAFT_NATIVE_CRASH_FAULT` says.
#[test]
fn child() {
let Some(dir) = std::env::var_os("SOLIDCRAFT_NATIVE_CRASH_CHILD") else { return };
let build = solidcraft_engine::crash::Build { version: "9.9.9", commit: "abc1234", built: "2026-10-25" };
let fresh = solidcraft_engine::crash::install(PathBuf::from(dir), build);
println!("FRESH {}", serde_json::to_string(&fresh).unwrap_or_default());
let Some(fault) = std::env::var_os("SOLIDCRAFT_NATIVE_CRASH_FAULT") else {
solidcraft_engine::crash::end_session();
return;
};
super::install();
solidcraft_engine::crash::phase("device creation");
match fault.to_str() {
Some("abort") => std::process::abort(),
Some("overflow") => println!("unreachable {}", recurse(0)),
Some("raise") => {
// SAFETY: none needed; this is the crash under test.
unsafe { libc::raise(SIGSEGV) };
println!("survived");
}
_ => segfault(),
}
}
/// Runs out of stack.
#[inline(never)]
fn recurse(n: u64) -> u64 {
let a = std::hint::black_box([n; 64]);
if n == u64::MAX { 0 } else { recurse(n + 1).wrapping_add(a[0]) }
}
/// The crash under test: a read of address 16, as a null struct pointer's field would be.
#[inline(never)]
fn segfault() {
// SAFETY: none; this is the crash under test.
let v = unsafe { std::ptr::read_volatile(std::hint::black_box(16usize) as *const u64) };
println!("unreachable {v}");
}
fn run(dir: &Path, fault: Option<&str>) -> std::process::Output {
let mut c = std::process::Command::new(std::env::current_exe().unwrap());
c.args(["--exact", "native_crash_macos::tests::child", "--test-threads=1", "--nocapture"]).env("SOLIDCRAFT_NATIVE_CRASH_CHILD", dir);
if let Some(f) = fault {
c.env("SOLIDCRAFT_NATIVE_CRASH_FAULT", f);
}
c.output().unwrap()
}
/// Crash a child the way `fault` says; returns what the handler wrote and the next start's report.
fn crash(name: &str, fault: &str, signal: c_int) -> (serde_json::Value, serde_json::Value, String) {
let dir = std::env::temp_dir().join(format!("solidcraft-native-crash-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let out = run(&dir, Some(fault));
assert_eq!(out.status.signal(), Some(signal), "the child died of the signal itself: {out:?}");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
let info =
std::fs::read_dir(dir.join("sessions")).unwrap().flatten().map(|e| e.path()).find(|p| p.to_string_lossy().ends_with(".native.json"));
let info = info.expect("the handler wrote what it caught");
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(info).unwrap()).unwrap();
// The next start reports it.
let out = run(&dir, None);
assert!(out.status.success(), "{out:?}");
let reports = solidcraft_engine::crash::list(&dir);
assert_eq!(reports.len(), 1, "{reports:?}");
let r = solidcraft_engine::crash::read(&dir, &reports[0].id).unwrap();
let _ = std::fs::remove_dir_all(&dir);
(v, r, stderr)
}
#[test]
fn a_segfault_is_reported_with_its_signal_module_frames_and_step() {
let (v, r, _) = crash("segv", "segv", SIGSEGV);
assert_eq!(v["signal"], "SIGSEGV", "{v}");
// Address 16 is in __PAGEZERO, mapped without access: SEGV_ACCERR.
assert_eq!(v["detail"], "read of 0x0000000000000010 (SEGV_ACCERR)", "{v}");
assert!(v["module"].as_str().unwrap().contains("solidcraft"), "the fault is in the test binary: {v}");
let frames = v["frames"].as_array().unwrap();
assert!(frames.len() >= 3, "{v}");
assert!(frames.iter().any(|f| f.as_str().unwrap().contains("child")), "the walk reaches the test that crashed: {v}");
let message = r["message"].as_str().unwrap();
assert!(message.contains("segmentation fault (SIGSEGV) in solidcraft"), "{r}");
assert!(message.contains("during startup (device creation"), "{r}");
assert_eq!(r["kind"], "unclean_exit");
assert_eq!(r["native"]["signal"], "SIGSEGV");
assert!(r["backtrace"].as_array().unwrap().iter().any(|f| f.as_str().unwrap().contains("segfault")), "{r}");
}
#[test]
fn an_abort_is_reported_as_sigabrt_with_the_code_that_called_it() {
let (v, r, _) = crash("abrt", "abort", SIGABRT);
assert_eq!(v["signal"], "SIGABRT", "{v}");
assert!(v.get("detail").is_none(), "{v}");
let frames = v["frames"].as_array().unwrap();
assert!(frames.iter().any(|f| f.as_str().unwrap().contains("abort")), "{v}");
assert!(r["message"].as_str().unwrap().contains("abort (SIGABRT)"), "{r}");
}
#[test]
fn a_stack_overflow_is_recorded_and_the_standard_library_still_says_so() {
// The fault on the guard page is recorded on the alternate stack; the standard library's
// handler, called after ours, prints its message and aborts.
let (v, r, stderr) = crash("overflow", "overflow", SIGABRT);
assert!(matches!(v["signal"].as_str(), Some("SIGSEGV" | "SIGBUS")), "{v}");
assert_eq!(v["name"], "stack overflow", "{v}");
assert!(v["detail"].as_str().unwrap().contains("below the thread's stack"), "{v}");
assert!(stderr.contains("has overflowed its stack"), "{stderr}");
assert!(r["message"].as_str().unwrap().contains("stack overflow ("), "{r}");
}
#[test]
fn a_segfault_signal_that_was_only_sent_still_ends_the_app() {
// The standard library's handler, which comes before ours, returns expecting the fault
// to happen again; a sent signal never does, so it goes to the default action instead.
let (v, r, _) = crash("raise", "raise", SIGSEGV);
assert_eq!(v["detail"], "sent with kill or raise, not a fault", "{v}");
assert!(r["message"].as_str().unwrap().contains("(SIGSEGV)"), "{r}");
}
}
+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. |
+4
View File
@@ -45,3 +45,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"
@@ -63,6 +63,7 @@ combine false
compare false
component false
compute false
cone false
configuration_table false
conic false
constrainer false
+47
View File
@@ -101,6 +101,7 @@
"error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"explode": "2aecb05d5f05aff1ab629066cb9bea8718ea213ac70948ef71a3ced3321f4cb2",
"export": "c8c2abf007e43e896479d78798e3976ba9eda809507c820bb38538f77db696ac",
"export_tray": "f18606e1f63726c57a0787dced4fb8a221b4a4fc18b88b0bbdee743e546cb489",
"extend": "28c63df352c77d02feffe44cf09fb0fc1d48f7905aaabbfa6dba601dbb70f8e8",
"extrude": "9cc1538971c5d25467aaed23f739899d9d102fd07f86472ac3197c54ca2e1144",
"eye": "2449167c8410826dc1106b687c12a309d5bea8f7817b897943e80f182a163c2d",
@@ -122,6 +123,7 @@
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
"home": "ab5f1c41d377687e4c5a808dfdccf2757b33dd43f934d44df43d6e9118176853",
"import": "e343540eb49fdb1e53c4d54b9e60ae513b3103df6c9133048bed878e58067455",
"import_tray": "c6aec9556dc8ecb6c953d6bf1f384f220c68167a92e444950c24ded81f66a0b6",
"include": "f5b80ad3e06e3b863fcfbe415ec912e4e09653dd519f9ac207819339fe34730f",
"infinite": "53fb781131078d2f8e71be23aa610d9f9d9c8bb79ba5f97fa397475e9ff8c3bd",
"inspect": "d2b4644b2d5f1aa50cb576b9265e275fa91201e296c2bc79865f810067d538e9",
@@ -206,8 +208,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 +263,17 @@
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
"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,6 +307,7 @@
"status_error": "b7188fcdfadf1512ddd2896805a14618179789f87047b219560a7dab74bd360f",
"status_warning": "1ac4f0ccfac2c01f6f66cb42188f9ae053c4aef9c5bbd249f1fb09611eb4d825",
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Cone</title>
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="#328bd3"/>
<path d="M32 5L39 59.82A20 11.55 0 0 0 51.3 45.97Z" fill="#236ead"/>
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 388 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

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Lens — the camera lens and its field of view</title>
<circle cx="32" cy="32" r="27" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="19" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="32" r="8.5" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="21.75" cy="21.75" r="3" fill="#e2e8ef"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Appearance library — looks on a shelf</title>
<circle cx="13" cy="28.35" r="9.5" fill="#b8c3cf"/>
<path d="M21.93 25.1A9.5 9.5 0 0 1 9.75 37.28A12.42 12.42 0 0 0 21.93 25.1Z" fill="#8e9faf"/>
<circle cx="9.77" cy="25.12" r="2.57" fill="#e2e8ef"/>
<circle cx="13" cy="28.35" r="9.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="51" cy="28.35" r="9.5" fill="#b8c3cf"/>
<path d="M59.93 25.1A9.5 9.5 0 0 1 47.75 37.28A12.42 12.42 0 0 0 59.93 25.1Z" fill="#8e9faf"/>
<circle cx="47.77" cy="25.12" r="2.57" fill="#e2e8ef"/>
<circle cx="51" cy="28.35" r="9.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="28.35" r="13.97" fill="#ffffff"/>
<circle cx="32" cy="28.35" r="11.5" fill="#328bd3"/>
<path d="M42.81 24.42A11.5 11.5 0 0 1 28.07 39.16A15.04 15.04 0 0 0 42.81 24.42Z" fill="#236ead"/>
<circle cx="28.09" cy="24.44" r="3.11" fill="#b9ddf8"/>
<circle cx="32" cy="28.35" r="11.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M7 42.35L57 42.35A2 2 0 0 1 59 44.35L59 46.35A2 2 0 0 1 57 48.35L7 48.35A2 2 0 0 1 5 46.35L5 44.35A2 2 0 0 1 7 42.35Z" fill="#8e9faf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Area light — angled panel and dotted direction stem</title>
<path d="M23.34 11L54.52 29L40.66 37L9.48 19Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Light by highlight — dotted aim stem and sphere target</title>
<circle cx="32" cy="9" r="2.125" fill="#236ead"/>
<circle cx="32" cy="16" r="2.125" fill="#236ead"/>
<circle cx="32" cy="23" r="2.125" fill="#236ead"/>
<circle cx="32" cy="42" r="15" fill="#e2e8ef"/>
<path d="M44.29 33.4A15 15 0 0 1 23.4 54.29A19.62 19.62 0 0 0 44.29 33.4Z" fill="#b8c3cf"/>
<circle cx="32" cy="42" r="15" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="37.5" r="5.5" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Point light — source disc and dotted direction stem</title>
<circle cx="32" cy="23" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="2.125" fill="#34465b"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Spot light — cone source and dotted direction stem</title>
<path d="M32 8L49 33Q32 43 15 33Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Lighting presets — a stack of ready-made light set-ups</title>
<path d="M24 5L54 5A5 5 0 0 1 59 10L59 40A5 5 0 0 1 54 45L24 45A5 5 0 0 1 19 40L19 10A5 5 0 0 1 24 5Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 17L42 17A5 5 0 0 1 47 22L47 54A5 5 0 0 1 42 59L10 59A5 5 0 0 1 5 54L5 22A5 5 0 0 1 10 17Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.3 41.55A11.5 11.5 0 1 1 33.7 41.55L33.2 45.55L18.8 45.55Z" fill="#b9ddf8" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21.3 45.55L30.7 45.55A2.5 2.5 0 0 1 33.2 48.05L33.2 50.05A2.5 2.5 0 0 1 30.7 52.55L21.3 52.55A2.5 2.5 0 0 1 18.8 50.05L18.8 48.05A2.5 2.5 0 0 1 21.3 45.55Z" fill="#8e9faf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Output size — the picture size in pixels</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" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.08 38.79L41.92 25.21" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M51 19L45.33 31.97L36.86 19.59Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13 45L18.67 32.03L27.14 44.41Z" fill="#236ead" stroke="#236ead" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Paint — flat brush with look-coloured bristles</title>
<path d="M26.13 59.09L22.24 55.2L22.94 48.83L16.58 49.54L14.46 47.42L15.17 41.06L8.8 41.76L4.91 37.87L19.06 23.73L40.27 44.94Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.24 20.55L28.25 14.54L37.09 16.31L46.28 7.11A7.5 7.5 0 0 1 56.89 17.72L47.69 26.91L49.46 35.75L43.45 41.76Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 589 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Pick Up — eyedropper with a look-coloured tube</title>
<path d="M8.96 52.22Q15.32 44.44 14.97 37.01L28.4 23.58L40.42 35.6L26.99 49.03Q19.56 48.68 11.78 55.04Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30.52 20.04L39.01 11.56A9.5 9.5 0 0 1 52.44 24.99L43.96 33.48Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25.22 18.98L29.46 14.74L49.26 34.54L45.02 38.78Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 682 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render quality — draft, balanced or high</title>
<path d="M10 38L17 38A3.5 3.5 0 0 1 20.5 41.5L20.5 52.5A3.5 3.5 0 0 1 17 56L10 56A3.5 3.5 0 0 1 6.5 52.5L6.5 41.5A3.5 3.5 0 0 1 10 38Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 24L35.5 24A3.5 3.5 0 0 1 39 27.5L39 52.5A3.5 3.5 0 0 1 35.5 56L28.5 56A3.5 3.5 0 0 1 25 52.5L25 27.5A3.5 3.5 0 0 1 28.5 24Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M47 8L54 8A3.5 3.5 0 0 1 57.5 11.5L57.5 52.5A3.5 3.5 0 0 1 54 56L47 56A3.5 3.5 0 0 1 43.5 52.5L43.5 11.5A3.5 3.5 0 0 1 47 8Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 848 B

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Replace All — every use of a look switched to another</title>
<circle cx="12.5" cy="13.3" r="10" fill="#b8c3cf"/>
<path d="M21.9 9.88A10 10 0 0 1 9.08 22.7A13.08 13.08 0 0 0 21.9 9.88Z" fill="#8e9faf"/>
<circle cx="9.1" cy="9.9" r="2.7" fill="#e2e8ef"/>
<circle cx="12.5" cy="13.3" r="10" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M36 36.8L25.39 34.68L33.88 26.19Z" fill="#236ead"/>
<path d="M21 21.8L30.34 31.14" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="52.5" cy="35.3" r="9" fill="#328bd3"/>
<path d="M60.96 32.22A9 9 0 0 1 49.42 43.76A11.77 11.77 0 0 0 60.96 32.22Z" fill="#236ead"/>
<circle cx="49.44" cy="32.24" r="2.43" fill="#b9ddf8"/>
<circle cx="52.5" cy="35.3" r="9" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="46.5" cy="47.3" r="14.68" fill="#ffffff"/>
<circle cx="46.5" cy="47.3" r="12" fill="#328bd3"/>
<path d="M57.78 43.2A12 12 0 0 1 42.4 58.58A15.69 15.69 0 0 0 57.78 43.2Z" fill="#236ead"/>
<circle cx="42.42" cy="43.22" r="3.24" fill="#b9ddf8"/>
<circle cx="46.5" cy="47.3" r="12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Render results — open the finished renders</title>
<path d="M21 6L55 6A5 5 0 0 1 60 11L60 35A5 5 0 0 1 55 40L21 40A5 5 0 0 1 16 35L16 11A5 5 0 0 1 21 6Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 17.5L43 17.5A7.5 7.5 0 0 1 50.5 25L50.5 51A7.5 7.5 0 0 1 43 58.5L9 58.5A7.5 7.5 0 0 1 1.5 51L1.5 25A7.5 7.5 0 0 1 9 17.5Z" fill="#ffffff"/>
<path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="#e2e8ef"/>
<defs><clipPath id="res"><path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="none"/></clipPath></defs>
<g clip-path="url(#res)">
<path d="M4 48.8L17.2 35.12L29.52 46.64L36.56 40.88L48 50.96L48 56L4 56Z" fill="#328bd3"/>
</g>
<path d="M9 20L43 20A5 5 0 0 1 48 25L48 51A5 5 0 0 1 43 56L9 56A5 5 0 0 1 4 51L4 25A5 5 0 0 1 9 20Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Same Look — the bodies wearing this look selected</title>
<path d="M6.3 41.5L15.83 47L15.83 58L6.3 52.5Z" fill="#b8c3cf"/>
<path d="M25.36 41.5L15.83 47L15.83 58L25.36 52.5Z" fill="#8e9faf"/>
<path d="M15.83 36L25.36 41.5L15.83 47L6.3 41.5Z" fill="#e2e8ef"/>
<path d="M15.83 36L25.36 41.5L25.36 52.5L15.83 58L6.3 52.5L6.3 41.5Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6.3 41.5L15.83 47L25.36 41.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15.83 47L15.83 58" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 12L45.83 17L45.83 27L37.17 22Z" fill="#328bd3"/>
<path d="M54.48 12L45.83 17L45.83 27L54.48 22Z" fill="#236ead"/>
<path d="M45.83 7L54.48 12L45.83 17L37.17 12Z" fill="#b9ddf8"/>
<path d="M45.83 7L54.48 12L54.48 22L45.83 27L37.17 22L37.17 12Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 12L45.83 17L54.48 12" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M45.83 17L45.83 27" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.33 4L54.33 4A4.5 4.5 0 0 1 58.83 8.5L58.83 25.5A4.5 4.5 0 0 1 54.33 30L37.33 30A4.5 4.5 0 0 1 32.83 25.5L32.83 8.5A4.5 4.5 0 0 1 37.33 4Z" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 42L45.83 47L45.83 57L37.17 52Z" fill="#328bd3"/>
<path d="M54.48 42L45.83 47L45.83 57L54.48 52Z" fill="#236ead"/>
<path d="M45.83 37L54.48 42L45.83 47L37.17 42Z" fill="#b9ddf8"/>
<path d="M45.83 37L54.48 42L54.48 52L45.83 57L37.17 52L37.17 42Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.17 42L45.83 47L54.48 42" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M45.83 47L45.83 57" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.33 34L54.33 34A4.5 4.5 0 0 1 58.83 38.5L58.83 55.5A4.5 4.5 0 0 1 54.33 60L37.33 60A4.5 4.5 0 0 1 32.83 55.5L32.83 38.5A4.5 4.5 0 0 1 37.33 34Z" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.83" cy="17" r="11.5" fill="#328bd3"/>
<path d="M26.64 13.07A11.5 11.5 0 0 1 11.9 27.81A15.04 15.04 0 0 0 26.64 13.07Z" fill="#236ead"/>
<circle cx="11.92" cy="13.09" r="3.11" fill="#b9ddf8"/>
<circle cx="15.83" cy="17" r="11.5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shot — save the camera view as a named shot</title>
<path d="M9 19L12 19L15 12Q15.6 10 17.5 10L22.5 10Q24.4 10 25 12L28 19L43 19Q48 19 48 24L48 46Q48 51 43 51L9 51Q4 51 4 46L4 24Q4 19 9 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="26" cy="35" r="10.5" 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="M42 47.5L53 47.5M47.5 42L47.5 53" fill="none" stroke="#ffffff" stroke-width="3.4" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 759 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shots strip — show or hide the strip of shot thumbnails</title>
<path d="M8 19L56 19A5 5 0 0 1 61 24L61 40A5 5 0 0 1 56 45L8 45A5 5 0 0 1 3 40L3 24A5 5 0 0 1 8 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10.5 24.5L19.5 24.5A2.5 2.5 0 0 1 22 27L22 37A2.5 2.5 0 0 1 19.5 39.5L10.5 39.5A2.5 2.5 0 0 1 8 37L8 27A2.5 2.5 0 0 1 10.5 24.5Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27.5 24.5L36.5 24.5A2.5 2.5 0 0 1 39 27L39 37A2.5 2.5 0 0 1 36.5 39.5L27.5 39.5A2.5 2.5 0 0 1 25 37L25 27A2.5 2.5 0 0 1 27.5 24.5Z" fill="#328bd3" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44.5 24.5L53.5 24.5A2.5 2.5 0 0 1 56 27L56 37A2.5 2.5 0 0 1 53.5 39.5L44.5 39.5A2.5 2.5 0 0 1 42 37L42 27A2.5 2.5 0 0 1 44.5 24.5Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Solo light — point source and dotted stem inside focus brackets</title>
<path d="M13.5 5L5.5 5L5.5 13M50.5 5L58.5 5L58.5 13M58.5 51.5L58.5 59.5L50.5 59.5M13.5 59.5L5.5 59.5L5.5 51.5" fill="none" stroke="currentColor" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="32" cy="23" r="2.125" fill="#34465b"/>
<circle cx="32" cy="44" r="2.125" fill="#236ead"/>
<circle cx="32" cy="51" r="2.125" fill="#236ead"/>
<circle cx="32" cy="58" r="2.125" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sun — source disc with short radial rays</title>
<circle cx="32" cy="32" r="15" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 5.5L32 10.5M32 53.5L32 58.5M5.5 32L10.5 32M53.5 32L58.5 32M13.26 13.26L16.8 16.8M47.2 47.2L50.74 50.74M13.26 50.74L16.8 47.2M47.2 16.8L50.74 13.26" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Transparent background — render with an alpha background</title>
<path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="#e2e8ef"/>
<defs><clipPath id="chk"><path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="none"/></clipPath></defs>
<g clip-path="url(#chk)">
<path d="M6 19L19 19L19 32L6 32ZM6 45L19 45L19 58L6 58ZM19 6L32 6L32 19L19 19ZM19 32L32 32L32 45L19 45ZM32 19L45 19L45 32L32 32ZM32 45L45 45L45 58L32 58ZM45 6L58 6L58 19L45 19ZM45 32L58 32L58 45L45 45Z" fill="#b9ddf8"/>
</g>
<path d="M11 6L53 6A5 5 0 0 1 58 11L58 53A5 5 0 0 1 53 58L11 58A5 5 0 0 1 6 53L6 11A5 5 0 0 1 11 6Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 18L44.12 25L44.12 39L32 46L19.88 39L19.88 25Z" fill="#ffffff" stroke="#ffffff" stroke-width="7.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M19.88 25L32 32L32 46L19.88 39Z" fill="#b8c3cf"/>
<path d="M44.12 25L32 32L32 46L44.12 39Z" fill="#8e9faf"/>
<path d="M32 18L44.12 25L32 32L19.88 25Z" fill="#e2e8ef"/>
<path d="M32 18L44.12 25L44.12 39L32 46L19.88 39L19.88 25Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M19.88 25L32 32L44.12 25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 46" fill="none" stroke="#34465b" stroke-width="2.55" 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>Update shot — overwrite the active shot with the current camera</title>
<path d="M9 19L12 19L15 12Q15.6 10 17.5 10L22.5 10Q24.4 10 25 12L28 19L43 19Q48 19 48 24L48 46Q48 51 43 51L9 51Q4 51 4 46L4 24Q4 19 9 19Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="26" cy="35" r="10.5" 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="M50.97 43.36A5.4 5.4 0 1 1 43.08 44.4" fill="none" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M46.06 40.14L39.8 42.11L46.35 46.7Z" fill="#ffffff" stroke="#ffffff" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 930 B

File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -457,6 +457,7 @@
"cmd.canvas_delete": "Smazat plátno",
"cmd.canvas_edit": "Upravit plátno",
"cmd.canvas_insert": "Plátno",
"cmd.canvas_to_sketch": "Náčrt z plátna",
"cmd.component_activate": "Aktivovat komponentu",
"cmd.component_break_link": "Zrušit odkaz",
"cmd.component_capture_position": "Zachytit polohu",
@@ -534,6 +535,7 @@
"cmd.file_export": "Exportovat",
"cmd.file_insert_mesh": "Vložit síť",
"cmd.file_insert_step": "Vložit STEP",
"cmd.file_open_in_easyprint": "Otevřít v EasyPrint",
"cmd.file_save_mesh": "Uložit jako síť",
"cmd.file_slicers": "Nainstalované slicery",
"cmd.frame_member": "Prvek rámu",
@@ -901,7 +903,7 @@
"home.new_mm": "Nový návrh",
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
"home.open": "Otevřít…",
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
"home.open_hint": "Návrh ve formátu .sc ({shortcut})",
"home.open_now": "Otevřít nyní",
"home.opened_days_ago": "Před {count} dny",
"home.opened_hours_ago": "Před {count} h",
@@ -1014,6 +1016,12 @@
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
"prefs.cpu": "CPU (softwarově)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Sledovat soubor motivu",
"prefs.theme_follow_error": "Nepoužito: {error}. Zůstává poslední funkční motiv.",
"prefs.theme_follow_launch": "Sledování jen pro toto spuštění (--theme-file); načte se znovu při každém přepsání jiným nástrojem.",
"prefs.theme_follow_note": "Načte se znovu při každém přepsání jiným nástrojem. Výběr motivu sledování ukončí.",
"prefs.theme_follow_off": "Vypnuto",
"prefs.theme_follow_stop": "Ukončit sledování",
"properties.title": "Vlastnosti: {name}",
"recovery.days_ago": "před {count} dny",
"recovery.detail": "{file} · prvků: {features} · automaticky uloženo {when}",
@@ -1493,7 +1501,7 @@
"ui.agent_chat_pop_out": "Otevřít ve vlastním okně, které lze přesunout na jinou obrazovku",
"ui.agent_chat_previous_picture": "Předchozí obrázek",
"ui.agent_chat_privacy": "Nic se nikam neodesílá, jen agentovi, kterého spouštíte pod vlastním účtem.",
"ui.agent_chat_privacy_note": "SolidCraft nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
"ui.agent_chat_privacy_note": "Chat sám nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
"ui.agent_chat_queue": "Zařadit",
"ui.agent_chat_queue_full": "Čeká příliš mnoho zpráv: některou odeberte nebo počkejte na agenta.",
"ui.agent_chat_queued": "Ve frontě",
@@ -1678,6 +1686,7 @@
"ui.command_done": "{command} hotovo",
"ui.command_palette_hint": "Zadejte příkaz, např. {example} — nebo stiskněte S pro hledání",
"ui.concentric": "Soustřednost",
"ui.cone": "Kužel",
"ui.conic_curve": "Kuželosečka",
"ui.constant": "Konstantní",
"ui.constraints_conflict": "Konflikt vazeb",
@@ -1736,6 +1745,11 @@
"ui.draft": "Úkos",
"ui.drawing": "Výkres",
"ui.drill_point": "Špička vrtáku",
"ui.easyprint_consent": "EasyPrint je slicer od Printables v prohlížeči. Aby se v něm model otevřel, SolidCraft jej nahraje na servery Printables. Odkaz vyprší po 30 minutách a musíte být přihlášeni k Printables. Pokračovat?",
"ui.easyprint_failed": "Nahrání do EasyPrint se nezdařilo: {error}",
"ui.easyprint_opened": "Otevřeno v EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, v prohlížeči)",
"ui.easyprint_uploading": "Nahrávám na Printables…",
"ui.edit": "Upravit",
"ui.editJointLimits": "Upravit limity kloubu",
"ui.edit_section_analysis": "Upravit analýzu řezu",
@@ -1793,6 +1807,7 @@
"ui.heading_circular_pattern": "KRUHOVÉ POLE",
"ui.heading_coil": "ŠROUBOVICE",
"ui.heading_combine": "KOMBINOVAT",
"ui.heading_cone": "KUŽEL",
"ui.heading_configure": "KONFIGURACE",
"ui.heading_constraints": "VAZBY",
"ui.heading_construct": "KONSTRUKCE",
@@ -2030,9 +2045,7 @@
"ui.right": "Zprava",
"ui.save": "Uložit",
"ui.save_as": "Uložit jako",
"ui.save_mesh_no_slicer": "Nebyl nalezen žádný slicer: zvolte Vlastní… a vyberte svůj slicer, nebo vypněte Odeslat do sliceru",
"ui.save_mesh_pick_bodies": "Vyberte tělesa k uložení",
"ui.save_mesh_temp_failed": "Nelze vytvořit {folder}: {error}",
"ui.saved_to": "Uloženo {path}",
"ui.sbox_pin": "Připnout nahoru ve vyhledávání S",
"ui.sbox_unpin": "Odepnout",
@@ -2047,7 +2060,6 @@
"ui.selection_shift_add_only": "Shift+klik pouze přidává",
"ui.selection_shift_add_only_tip": "Zapnuto: Shift přidává položky, Ctrl/Cmd přepíná. Vypnuto (výchozí): Shift i Ctrl/Cmd přepínají.",
"ui.send": "Odeslat",
"ui.send_to_slicer": "Odeslat do sliceru",
"ui.sent_to_slicer": "Odesláno do {slicer}",
"ui.sheet_gap": "Mezera",
"ui.shell": "Skořepina",
@@ -2139,6 +2151,7 @@
"ui.untitled": "Bez názvu",
"ui.up_axis_note": "Která osa v pohledech míří nahoru (pohled podél osy, výchozí pohled, pohled kolmo). Návrhy se nemění.",
"ui.updateToCurrentView": "Aktualizovat na aktuální pohled",
"ui.upload": "Nahrát",
"ui.view_centre": "Střed pohledu",
"ui.visual_shaded_hidden_edges": "Stínovaný se skrytými hranami",
"ui.visual_shaded_with_edges": "Stínovaný s hranami",
+18 -5
View File
@@ -649,6 +649,7 @@
"cmd.canvas_delete": "Slet referencebillede",
"cmd.canvas_edit": "Redigér referencebillede",
"cmd.canvas_insert": "Referencebillede",
"cmd.canvas_to_sketch": "Skitse fra referencebillede",
"cmd.component_activate": "Aktivér komponent",
"cmd.component_break_link": "Bryd forbindelse",
"cmd.component_capture_position": "Registrér position",
@@ -729,6 +730,7 @@
"cmd.file_export": "Eksportér",
"cmd.file_insert_mesh": "Indsæt mesh",
"cmd.file_insert_step": "Indsæt STEP",
"cmd.file_open_in_easyprint": "Åbn med EasyPrint",
"cmd.file_save_mesh": "Gem som mesh",
"cmd.file_slicers": "Installerede slicere",
"cmd.frame_member": "Rammeelement",
@@ -1104,7 +1106,7 @@
"home.new_mm": "Nyt design",
"home.new_mm_hint": "Millimeter · åbner i Design",
"home.open": "Åbn…",
"home.open_hint": "Et .solidcraft-design ({shortcut})",
"home.open_hint": "Et .sc-design ({shortcut})",
"home.open_now": "Åbn nu",
"home.opened_days_ago": "For {count} dage siden",
"home.opened_hours_ago": "For {count} t siden",
@@ -1322,6 +1324,12 @@
"prefs.supersampling_brief": "Tegner visningen i større størrelse og nedskalerer den med filtrering: meget skarpt og mere krævende.",
"prefs.tab_accent_note": "Accentfarven følger den aktive fane",
"prefs.theme_file": "temafil",
"prefs.theme_follow": "Følg en temafil",
"prefs.theme_follow_error": "Ikke anvendt: {error}. Det seneste gyldige tema bevares.",
"prefs.theme_follow_launch": "Følges kun denne gang (--theme-file); genindlæses, når et andet værktøj overskriver filen.",
"prefs.theme_follow_note": "Genindlæses, når et andet værktøj overskriver filen. Valg af et tema stopper dette.",
"prefs.theme_follow_off": "Fra",
"prefs.theme_follow_stop": "Stop med at følge",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nu",
"prefs.timeline_quarter_hour": "−15",
@@ -1670,7 +1678,7 @@
"ui.agent_chat_pop_out": "Åbn i eget vindue, som du kan flytte til en anden skærm",
"ui.agent_chat_previous_picture": "Forrige billede",
"ui.agent_chat_privacy": "Intet sendes andre steder end til den agent, du kører, under din egen konto.",
"ui.agent_chat_privacy_note": "SolidCraft sender intet nogen steder: Agenten, du kører, kommunikerer med sin egen tjeneste under din egen konto. SolidCraft ser eller gemmer aldrig en nøgle eller adgangskode.",
"ui.agent_chat_privacy_note": "Chatten sender selv intet nogen steder: Agenten, du kører, kommunikerer med sin egen tjeneste under din egen konto. SolidCraft ser eller gemmer aldrig en nøgle eller adgangskode.",
"ui.agent_chat_queue": "Sæt i kø",
"ui.agent_chat_queue_full": "For mange beskeder venter: Fjern en, eller vent på agenten.",
"ui.agent_chat_queued": "I kø",
@@ -1863,6 +1871,7 @@
"ui.command_done": "{command} udført",
"ui.command_palette_hint": "Skriv en kommando, f.eks. {example} — eller tryk S for at søge",
"ui.concentric": "Koncentrisk",
"ui.cone": "Kegle",
"ui.conic_curve": "Keglesnitskurve",
"ui.constant": "Konstant",
"ui.constraints_conflict": "Bindinger er i konflikt",
@@ -1921,6 +1930,11 @@
"ui.draft": "Slipvinkel",
"ui.drawing": "Tegning",
"ui.drill_point": "Borspids",
"ui.easyprint_consent": "EasyPrint er Printables' slicer i browseren. For at åbne modellen der uploader SolidCraft den til Printables' servere. Linket udløber efter 30 minutter, og du skal være logget ind på Printables. Fortsæt?",
"ui.easyprint_failed": "Upload til EasyPrint mislykkedes: {error}",
"ui.easyprint_opened": "Åbnet i EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, i browseren)",
"ui.easyprint_uploading": "Uploader til Printables…",
"ui.edit": "Redigér",
"ui.editJointLimits": "Redigér ledgrænser",
"ui.edit_section_analysis": "Redigér snitanalyse",
@@ -1982,6 +1996,7 @@
"ui.heading_circular_pattern": "CIRKULÆRT MØNSTER",
"ui.heading_coil": "SPIRAL",
"ui.heading_combine": "KOMBINÉR",
"ui.heading_cone": "KEGLE",
"ui.heading_configure": "KONFIGURÉR",
"ui.heading_constraints": "BINDINGER",
"ui.heading_construct": "KONSTRUKTION",
@@ -2232,9 +2247,7 @@
"ui.right": "Fra højre",
"ui.save": "Gem",
"ui.save_as": "Gem som",
"ui.save_mesh_no_slicer": "Ingen slicer fundet: Vælg Brugerdefineret…, og vælg dit slicerprogram, eller slå Send til slicer fra",
"ui.save_mesh_pick_bodies": "Vælg de emner, der skal gemmes",
"ui.save_mesh_temp_failed": "Kunne ikke oprette {folder}: {error}",
"ui.saved_to": "Gemt: {path}",
"ui.sbox_pin": "Fastgør øverst i S-menuen",
"ui.sbox_unpin": "Fjern fastgørelse",
@@ -2249,7 +2262,6 @@
"ui.selection_shift_add_only": "Shift+klik tilføjer uden at fjerne",
"ui.selection_shift_add_only_tip": "Slået til: Shift tilføjer til markeringen. Ctrl/Cmd tilføjer eller fjerner. Slået fra (standard): Både Shift og Ctrl/Cmd tilføjer eller fjerner.",
"ui.send": "Send",
"ui.send_to_slicer": "Send til slicer",
"ui.sent_to_slicer": "Sendt til {slicer}",
"ui.sheet_gap": "Mellemrum",
"ui.shell": "Udhulning",
@@ -2347,6 +2359,7 @@
"ui.untitled": "Uden titel",
"ui.up_axis_note": "Hvilken akse der peger opad i visningerne (aksevisning, Hjem, Se på). Designs ændres ikke.",
"ui.updateToCurrentView": "Opdatér til aktuel visning",
"ui.upload": "Upload",
"ui.view_centre": "Visningens centrum",
"ui.visual_shaded_hidden_edges": "Skyggelagt med skjulte kanter",
"ui.visual_shaded_with_edges": "Skyggelagt med kanter",
+330 -6
View File
@@ -5,7 +5,10 @@
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Screenshot anhängen: kopieren und einfügen oder als PNG speichern und hochladen.",
"2D": "2D",
"3. Click Submit new issue on GitHub to send your report.": "3. Auf GitHub auf Submit new issue klicken, um den Bericht zu senden.",
"A Single hole snaps to the middle of its face and to the centres of its round edges": "Eine einzelne Bohrung rastet am Flächenmittelpunkt und an den Mittelpunkten ihrer kreisförmigen Kanten ein",
"A new configuration, a copy of the active one": "Eine neue Konfiguration, eine Kopie der aktiven",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "Eine platzierte einzelne Bohrung lässt sich über einen Ziehgriff verschieben. Durch erneutes Anklicken ihrer Fläche wird die Bohrung dorthin verschoben, statt entfernt zu werden",
"A short title for the problem (optional)": "Kurzer Problemtitel (optional)",
"ARRANGE": "ANORDNEN",
"About SolidCraft": "Über SolidCraft",
"Acceleration": "Beschleunigung",
@@ -19,19 +22,27 @@
"Add Mid Radius": "Mittleren Radius hinzufügen",
"Add Row": "Zeile hinzufügen",
"Add a tool": "Werkzeug hinzufügen",
"Add all bodies": "Alle Körper hinzufügen",
"Add bend": "Biegung hinzufügen",
"Add condition": "Bedingung hinzufügen",
"Add or edit rule": "Regel hinzufügen oder bearbeiten",
"Add to New Folder": "Zu neuem Ordner hinzufügen",
"Add visible bodies": "Sichtbare Körper hinzufügen",
"Advanced": "Erweitert",
"Against": "Gegen",
"Alignment": "Ausrichtung",
"All Lines": "Alle Linien",
"All Sketch Points": "Alle Skizzenpunkte",
"All components": "Alle Komponenten",
"All open designs": "Alle geöffneten Designs",
"Allowed expressions, separated by semicolons": "Zulässige Ausdrücke, durch Semikolons getrennt",
"Also deletes": "Löscht auch",
"Anchor": "Anker",
"Angle 2": "Winkel 2",
"Angle Display": "Winkelanzeige",
"Animate": "Animieren",
"Another bend on the far edge of the last flange": "Eine weitere Biegung an der gegenüberliegenden Kante des letzten Flansches",
"App/system information and command settings are included. Review private details before sharing.": "App- und Systeminformationen sowie Befehlseinstellungen werden mit übermittelt. Prüfen Sie vor dem Teilen, ob private Angaben enthalten sind.",
"Appearance name": "Name des Erscheinungsbilds",
"Applied Radius": "Angewendeter Radius",
"Apply To": "Anwenden auf",
@@ -46,6 +57,8 @@
"BLEND SURFACE": "ÜBERGANGSFLÄCHE",
"BOUNDARY SURFACE": "BERANDUNGSFLÄCHE",
"BROWSER": "BROWSER",
"Bar": "Flachstab",
"Bend": "Biegung",
"Bend Edges": "Biegekanten",
"Bend Line Position": "Position der Biegelinie",
"Bend Position": "Biegeposition",
@@ -58,6 +71,9 @@
"Body (last if none)": "Körper (letzter, wenn keiner)",
"Body to split": "Zu teilender Körper",
"Body, or faces to take out": "Körper oder zu entfernende Flächen",
"Bottom Left": "Unten links",
"Bottom Middle": "Unten Mitte",
"Bottom Right": "Unten rechts",
"Boundary": "Begrenzung",
"Bounding box": "Hüllquader",
"CHECK": "PRÜFEN",
@@ -77,12 +93,15 @@
"Center Y": "Mittelpunkt Y",
"Center of mass": "Massenschwerpunkt",
"Centre to centre": "Mitte zu Mitte",
"Centroid": "Flächenschwerpunkt",
"Chamfer Type": "Abschrägungstyp",
"Channel": "U-Profil",
"Check collisions": "Kollisionen prüfen",
"Checked": "Geprüft",
"Choose Variable radius or Spline in Type.": "Unter „Typ“ „Variable radius“ oder „Spline“ wählen.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Wählen Sie eine neue Ebene. Skizzengeometrie und Bemaßungen bleiben in ihren zweidimensionalen Koordinaten unverändert.",
"Choose…": "Auswählen…",
"Circle center": "Kreismittelpunkt",
"Circular": "Kreisförmig",
"Circumference": "Umfang",
"Clockwise": "Im Uhrzeigersinn",
@@ -102,8 +121,10 @@
"Contact": "Kontakt",
"Contact Sets": "Kontaktsätze",
"Contact sets": "Kontaktsätze",
"Continue on GitHub": "Auf GitHub fortfahren",
"Continuity": "Stetigkeit",
"Control Radius": "Kontrollradius",
"Copies the definition only. Assign it from Physical Material when ready.": "Kopiert nur die Materialdefinition. Weisen Sie das Material anschließend unter „Physikalisches Material“ zu.",
"Copy Error Message": "Fehlermeldung kopieren",
"Copy Warning Message": "Warnmeldung kopieren",
"Copy all licences": "Alle Lizenzen kopieren",
@@ -112,23 +133,29 @@
"Copy screenshot": "Screenshot kopieren",
"Corner Relief": "Eckenentlastung",
"Corner Relief Shape": "Form der Eckenentlastung",
"Corner radius": "Eckenradius",
"Counterbore depth": "Tiefe der Senkung",
"Counterbore Ø": "Tiefenbohrung Ø",
"Countersink angle": "Winkel der Kegelsenkung",
"Countersink Ø": "Senkung Ø",
"Create Hole": "Bohrung erstellen",
"Create Issue — review draft": "Issue erstellen — Entwurf prüfen",
"Create Issue…": "Issue erstellen…",
"Cross curves": "Querkurven",
"Ctrl+Enter continues on GitHub.": "Strg+Enter geht weiter zu GitHub.",
"Cube": "Würfel",
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
"Current perspective view": "Aktuelle perspektivische Ansicht",
"Current plane": "Aktuelle Ebene",
"Curv. Comb": "Krümmungskamm",
"Curvature (G2)": "Krümmung (G2)",
"Curvature change (G3)": "Krümmungsänderung (G3)",
"Custom": "Benutzerdefiniert",
"Customize Toolbar": "Symbolleiste anpassen",
"Customize…": "Anpassen…",
"Cylindrical face": "Zylindrische Fläche",
"Delete": "Löschen",
"Delete Folders and Features…": "Ordner mit Features löschen…",
"Delete Selected Features…": "Ausgewählte Merkmale löschen…",
"Delete Selected Folders": "Ausgewählte Ordner löschen",
"Delete all": "Alle löschen",
@@ -137,6 +164,8 @@
"Delete the set": "Satz löschen",
"Density": "Dichte",
"Dependents": "Abhängige Elemente",
"Describe the problem you saw": "Beschreiben Sie das aufgetretene Problem",
"Describe what happened before continuing.": "Beschreiben Sie, was passiert ist, bevor Sie fortfahren.",
"Designs you open or save appear here.": "Designs, die Sie öffnen oder speichern, erscheinen hier.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestimmt durch Bis Objekt. Wählen Sie Winkel, um den Winkelbereich zu bearbeiten.",
"Diameter": "Durchmesser",
@@ -148,6 +177,7 @@
"Don't ask again": "Nicht mehr fragen",
"Double-click to rename": "Double-click to rename",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Doppelklick mit der mittleren Taste in der Ansicht: Home (Startansicht, zeigt alles Sichtbare), Einpassen (Sichtbares einpassen, Ausrichtung beibehalten) oder nichts",
"Drag to move the hole; click to remove it": "Ziehen, um die Bohrung zu verschieben; anklicken, um sie zu entfernen",
"Drive Joint": "Gelenk antreiben",
"Duration": "Dauer",
"Edges": "Kanten",
@@ -159,6 +189,7 @@
"End bulge": "Endausbauchung",
"End continuity": "Endstetigkeit",
"Expand": "Ausklappen",
"Expected result (optional)": "Erwartetes Ergebnis (optional)",
"Explode": "Explodieren",
"Explode Text": "Text auflösen",
"Export Animation…": "Animation exportieren…",
@@ -171,6 +202,7 @@
"Export JSON…": "JSON exportieren…",
"Export design library...": "Designbibliothek exportieren...",
"Export hidden edges as dashed lines": "Verdeckte Kanten gestrichelt exportieren",
"Exported by SolidWorks, which usually saves Y up": "Aus SolidWorks exportiert; dort zeigt die Y-Achse normalerweise nach oben",
"Expression": "Ausdruck",
"Extend to meet": "Verlängern, bis sie sich treffen",
"Extents": "Ausdehnung",
@@ -179,6 +211,7 @@
"Face": "Fläche",
"Face Appearance": "Flächenerscheinungsbild",
"Face attachment lost": "Flächenanbindung verloren",
"Face center": "Flächenmittelpunkt",
"Faces": "Flächen",
"Faces / edges / vertices": "Flächen / Kanten / Ecken",
"Faces to split": "Zu teilende Flächen",
@@ -194,6 +227,8 @@
"First plane": "Erste Ebene",
"Fit and G2 corners require Constant radius.": "Passung und G2-Ecken erfordern Constant radius.",
"Fixed Face": "Feste Fläche",
"Flange thickness": "Flanschdicke",
"Flanges": "Flansche",
"Flat Pattern": "Flachmuster",
"Flat size": "Abwicklungsgröße",
"Follow camera (Look At sketches)": "Kamera folgen (Skizzen ansehen)",
@@ -203,23 +238,32 @@
"From Bodies": "Aus Körpern",
"From edges": "Ausgangskanten",
"Full Length": "Volle Länge",
"Full thread depth": "Volle Gewindetiefe",
"Further Bends": "Weitere Biegungen",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-Ecken erfordern einen konstanten Radius und Exact fit. Die Konstruktion von G2-Ecken wird noch nicht unterstützt.",
"Gap": "Spalt",
"Grid": "Raster",
"HISTORY": "VERLAUF",
"Halo under sketch curves": "Hervorhebung unter Skizzenkurven",
"Height": "Höhe",
"Height From": "Höhe ab",
"Hidden edges": "Verdeckte Kanten",
"History layout": "Verlaufslayout",
"History names": "Namen im Konstruktionsverlauf",
"Hole (optional)": "Bohrung (optional)",
"Holes": "Löcher",
"Holes (edges on them)": "Löcher (ihre Kanten)",
"Home view": "Startansicht",
"Horiz./Vert.": "Horiz./Vert.",
"I-Beam": "I-Träger",
"IMAGES": "BILDER",
"Import material into design": "Material ins Design importieren",
"Import…": "Importieren…",
"In This Design": "In diesem Design",
"In place (surfaces)": "An Ort und Stelle (Flächen)",
"In plane": "In der Ebene",
"Included": "Enthalten",
"Inner Sharp": "Innenkante",
"Inside": "Innen",
"Instant": "Sofort",
"Interface font": "Schriftart der Oberfläche",
@@ -235,10 +279,12 @@
"Keep of the other surface": "Vom anderen Flächenkörper behalten",
"Key a joint's value at this step": "Gelenkwert in diesem Schritt als Schlüsselbild setzen",
"Keys": "Schlüsselbilder",
"L-Angle": "L-Profil",
"LIBRARY": "BIBLIOTHEK",
"LINKS": "VERKNÜPFUNGEN",
"Later": "Später",
"Leave it": "Belassen",
"Left Middle": "Links Mitte",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Alte Flächenanbindung. Das erneute Anbinden an eine zertifizierte Fläche erzeugt eine exakte Anbindung.",
"Length": "Länge",
"Length units": "Length units",
@@ -248,11 +294,13 @@
"Limits": "Grenzen",
"Line Spacing": "Zeilenabstand",
"Line Type": "Linientyp",
"Lines": "Linien",
"Listing the installed fonts…": "Installierte Schriftarten werden aufgelistet…",
"Load library": "Bibliothek laden",
"Local library": "Lokale Bibliothek",
"Loop": "Schleife",
"M5 or M5x0.5": "M5 oder M5x0.5",
"M5, M5x0.5, 1/4-20 UNC": "M5, M5x0.5, 1/4-20 UNC",
"MANAGE": "VERWALTEN",
"MATERIALS": "MATERIALIEN",
"MODEL": "MODELL",
@@ -262,6 +310,8 @@
"Mass Center": "Massenschwerpunkt",
"Matched restrictions check the final values.": "Zutreffende Einschränkungen prüfen die endgültigen Werte.",
"Material": "Material",
"Material Outside": "Material außen",
"Material copied. Existing body assignments are unchanged.": "Material kopiert. Bestehende Materialzuweisungen der Körper bleiben unverändert.",
"Max": "Max",
"Maximum": "Maximum",
"Mesh body": "Netzkörper",
@@ -281,9 +331,11 @@
"Move Type": "Verschiebungstyp",
"Move down": "Nach unten",
"Move earlier": "Nach vorn verschieben",
"Move holes by clicking or dragging": "Bohrungen durch Anklicken oder Ziehen verschieben",
"Move later": "Nach hinten verschieben",
"Move up": "Nach oben",
"Name": "Name",
"Name on import": "Name beim Import",
"Nearest": "Nächstgelegen",
"New": "Neu",
"New Comp.": "Neue Komp.",
@@ -293,22 +345,32 @@
"No crash reports.": "Keine Absturzberichte.",
"No matching appearance presets.": "Keine passenden Erscheinungsbildvorlagen.",
"No matching appearances": "Keine passenden Erscheinungsbilder",
"No matching physical materials.": "Keine passenden physikalischen Materialien gefunden.",
"No visible bodies or sketches to export.": "Keine sichtbaren Körper oder Skizzen zum Exportieren.",
"Nominal diameter": "Nenndurchmesser",
"None": "Keine",
"Not available yet": "Noch nicht verfügbar",
"Not saved yet": "Noch nicht gespeichert",
"Notch": "Ausklinkung",
"OUTPUT": "AUSGABE",
"Of": "Freiheitsgrad",
"On your computer": "Auf diesem Computer",
"Opacity": "Deckkraft",
"Open GitHub draft": "GitHub-Entwurf öffnen",
"Open bodies (no volume)": "Offene Körper (ohne Volumen)",
"Open library...": "Bibliothek öffnen...",
"Open local libraries in the desktop app. You can export this design's library here.": "Öffnen Sie lokale Bibliotheken in der Desktop-App. Die Bibliothek dieses Designs können Sie hier exportieren.",
"Open surface — no volume": "Offene Oberfläche – kein Volumen",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Quelloffenes parametrisches 3D-CAD in Rust. Lizenziert unter MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Öffnet den Browser und kopiert den Berichtstext, den Sie anschließend auf GitHub einfügen können.",
"Opens your browser. Review and submit your report on GitHub.": "Öffnet den Browser. Prüfen Sie den Bericht und reichen Sie ihn auf GitHub ein.",
"Operator": "Operator",
"Orbit (right drag)": "Drehen (mit rechts ziehen)",
"Other side": "Andere Seite",
"Other surface (mutual trim)": "Anderer Flächenkörper (gegenseitig trimmen)",
"Other way": "Entgegengesetzte Richtung",
"Outer Sharp": "Außenkante",
"Outer radius": "Außenradius",
"Outside": "Außen",
"Overall length": "Gesamtlänge",
"Param. Graph": "Param.-Graph",
@@ -318,9 +380,11 @@
"Path": "Pfad",
"Pattern": "Muster",
"Per Edge": "Pro Kante",
"Physical material definitions": "Definitionen physikalischer Materialien",
"Pick the tool bodies": "Werkzeugkörper wählen",
"Picked": "Ausgewählte",
"Picked point": "Gewählter Punkt",
"Pitch": "Steigung",
"Pivot": "Drehpunkt",
"Place at the origin instead": "Stattdessen im Ursprung platzieren",
"Plastic Rules": "Kunststoffregeln",
@@ -350,6 +414,7 @@
"Recover": "Wiederherstellen",
"Recover unsaved design?": "Nicht gespeichertes Design wiederherstellen?",
"Rectangular": "Rechteckig",
"Rectangular Tube": "Rechteckrohr",
"Reference face": "Referenzfläche",
"Remove Tools": "Werkzeuge entfernen",
"Remove condition": "Bedingung entfernen",
@@ -360,11 +425,15 @@
"Remove screenshot": "Screenshot entfernen",
"Remove the column": "Spalte entfernen",
"Remove the track": "Spur entfernen",
"Remove this bend": "Diese Biegung entfernen",
"Remove this point": "Diesen Punkt entfernen",
"Rename Feature": "Merkmal umbenennen",
"Rename Folder": "Ordner umbenennen",
"Render Stage": "Renderphase",
"Replace existing library file": "Vorhandene Bibliotheksdatei ersetzen",
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
"Report text copied. Paste it into GitHub and submit your report there.": "Berichtstext kopiert. Fügen Sie ihn auf GitHub ein und reichen Sie den Bericht dort ein.",
"Reports are public. A GitHub account is required.": "Berichte sind öffentlich. Ein GitHub-Konto ist erforderlich.",
"Reset Toolbar": "Symbolleiste zurücksetzen",
"Resize Part": "Teil skalieren",
"Restore View": "Ansicht wiederherstellen",
@@ -380,31 +449,40 @@
"Revert Pos.": "Pos. zurücksetzen",
"Revert Position": "Position zurücksetzen",
"Review the report and screenshot before opening GitHub.": "Prüfen Sie Bericht und Screenshot, bevor Sie GitHub öffnen.",
"Review your report on GitHub and click Submit new issue to publish it.": "Prüfen Sie Ihren Bericht auf GitHub und klicken Sie auf „Submit new issue“, um ihn zu veröffentlichen.",
"Rib Length": "Rippenlänge",
"Rib Offset": "Rippenversatz",
"Rib Thickness": "Rippendicke",
"Ribs": "Rippen",
"Right Middle": "Rechts Mitte",
"Rim Edge": "Randkante",
"Ring of tools on the right button": "Werkzeugring auf der rechten Maustaste",
"Roll Back Here": "Hierher zurücksetzen",
"Roll History Marker Here": "Verlaufsmarkierung hierher verschieben",
"Root radius": "Kehlradius",
"Round": "Rund",
"Round Bar": "Rundstab",
"Round Tube": "Rundrohr",
"Rule": "Rule",
"Rule name": "Regelname",
"Rules run from top to bottom. Matched drives change effective values.": "Regeln werden von oben nach unten ausgeführt. Zutreffende Steuerungen ändern die wirksamen Werte.",
"SECTION": "SCHNITT",
"SURFACE QUALITY": "OBERFLÄCHENQUALITÄT",
"Same way": "Gleiche Richtung",
"Save Rule": "Regel speichern",
"Save View": "Ansicht speichern",
"Save screenshot…": "Screenshot speichern…",
"Save the study as an animated GIF, seen as the viewport shows it": "Die Studie als animiertes GIF speichern, so wie der Ansichtsbereich sie zeigt",
"Saved preview. Geometry is being checked before editing.": "Gespeicherte Vorschau. Die Geometrie wird vor dem Bearbeiten geprüft.",
"Scale factor": "Skalierungsfaktor",
"Scene": "Szene",
"Screenshot (optional)": "Screenshot (optional)",
"Search": "Suchen",
"Search commands": "Befehle suchen",
"Search this library": "Diese Bibliothek durchsuchen",
"Second plane": "Zweite Ebene",
"Section": "Schnitt",
"Section (optional)": "Abschnitt (optional)",
"Section position": "Lage des Querschnitts",
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
"Select Other": "Andere auswählen",
@@ -419,6 +497,7 @@
"Shape": "Form",
"Sheet Rules": "Blechregeln",
"Show Agent Cursor": "Agenten-Cursor anzeigen",
"Show ghost": "Geistermodell anzeigen",
"Show in folder": "Im Ordner anzeigen",
"Show the pointer an AI agent moves (#35)": "Den Zeiger anzeigen, den ein KI-Agent bewegt (#35)",
"Show tool at cursor": "Werkzeug am Cursor anzeigen",
@@ -431,6 +510,7 @@
"Sketch plane has no body-face attachment.": "Die Skizzenebene hat keine Anbindung an eine Körperfläche.",
"Sketch points": "Skizzenpunkte",
"Slot width": "Langlochbreite",
"Snap to face and edge centres": "An Flächen- und Kantenmittelpunkten einrasten",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft wurde beendet, bevor diese Designs gespeichert wurden. Ihr letzter Autosave kann geöffnet werden.",
"Specialise": "Spezialisieren",
"Speed": "Geschwindigkeit",
@@ -444,23 +524,30 @@
"Start continuity": "Startstetigkeit",
"Step": "Schritt",
"Steps": "Schritte",
"Steps to reproduce (optional)": "Schritte zur Reproduktion (optional)",
"Stitch the kept pieces": "Behaltene Teile vernähen",
"Stitch to both bodies": "Mit beiden Körpern vernähen",
"Studio": "Studio",
"Study": "Untersuchung",
"Style": "Stil",
"Summary": "Zusammenfassung",
"Suppress": "Unterdrücken",
"Suppress Selected Folders": "Ausgewählte Ordner unterdrücken",
"Suppressed": "Unterdrückt",
"Surface": "Flächenkörper",
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
"Swap Ends": "Enden vertauschen",
"TRIM": "TRIMMEN",
"Tangent": "Tangential",
"Tangent (G1)": "Tangente (G1)",
"Tangent Chain": "Tangentenkette",
"Tangent chain": "Tangentialkette",
"Tangent to Edge": "Tangential an Kante",
"Tap drill diameter": "Kernlochdurchmesser",
"Target": "Ziel",
"Target Body": "Zielkörper",
"Tear": "Reißen",
"Technical details": "Technische Details",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
"Then": "Then",
@@ -472,20 +559,28 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Löschen entfernt es aus jeder Kopie des übergeordneten Elements.",
"This child is shared. Position changes apply to every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Positionsänderungen gelten für jede Kopie des übergeordneten Elements.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Diese Fläche kann keine dauerhafte Anbindung bieten. Wählen Sie eine andere Fläche oder verwenden Sie ausdrücklich eine feste Ebene.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Dieses Bild wurde noch nicht an GitHub angehängt. Kopieren oder speichern Sie es und fügen Sie es anschließend Ihrem GitHub-Bericht hinzu.",
"Thread depth": "Gewindetiefe",
"Thread diameter": "Gewindedurchmesser",
"To": "Bis",
"To Offset": "Bis Versatz",
"To edges": "Zielkanten",
"Toe radius": "Kantenradius",
"Tolerance": "Toleranz",
"Too close": "Zu nah",
"Tool Bodies": "Werkzeugkörper",
"Tools": "Werkzeuge",
"Tools, clockwise from the top": "Werkzeuge, im Uhrzeigersinn von oben",
"Top Left": "Oben links",
"Top Middle": "Oben Mitte",
"Top Right": "Oben rechts",
"Trimming tool (plane, face or curves)": "Trimmwerkzeug (Ebene, Fläche oder Kurven)",
"Turn off section analysis before exporting the view.": "Deaktivieren Sie die Schnittanalyse, bevor Sie die Ansicht exportieren.",
"Type": "Typ",
"UNTRIM": "TRIMMEN AUFHEBEN",
"Unit": "Einheit",
"Unsuppress": "Unterdrückung aufheben",
"Unsuppress Selected Folders": "Unterdrückung der ausgewählten Ordner aufheben",
"Updating the preview": "Vorschau wird aktualisiert",
"Use active author placement": "Aktive Autorplatzierung verwenden",
"Use fixed plane": "Feste Ebene verwenden",
@@ -504,6 +599,10 @@
"Wall": "Wand",
"Wall Location": "Wandlage",
"Wall Thickness": "Wandstärke",
"Web thickness": "Stegdicke",
"What did you do just before the problem? A short list is enough": "Was haben Sie unmittelbar vor dem Auftreten des Problems getan? Eine kurze Liste genügt.",
"What did you expect to see?": "Was hatten Sie erwartet?",
"What happened?": "Was ist passiert?",
"When": "When",
"Width": "Breite",
"Will fail": "Schlägt fehl",
@@ -522,6 +621,34 @@
"Z angle": "Z-Winkel",
"Z distance": "Z-Abstand",
"Zebra": "Zebra",
"ai.connected_toast": "SolidCraft ist jetzt in {clients} verfügbar. Starten Sie diese neu, um es zu nutzen (Einstellungen > Assistent).",
"ai.prefs.allow_control": "Verbindungen von KI-Agenten zulassen (Steuerkanal / MCP)",
"ai.prefs.allow_control_note": "Öffnet einen Steuerkanal nur auf diesem Computer (127.0.0.1, Port 27878 oder der nächste freie). Ein verbindendes Programm muss zusätzlich ein geheimes Token vorweisen, das nur Ihr Benutzerkonto lesen kann und bei jedem Start neu erzeugt wird. Wirkt ab dem nächsten Start von SolidCraft. Aus: Es wartet nichts auf Verbindungen, außer Sie starten SolidCraft mit --control.",
"ai.prefs.availability": "Assistenten-Steuerung",
"ai.prefs.background_commands": "Agentenbefehle im Hintergrund ausführen",
"ai.prefs.connect_apps": "SolidCraft für KI-Apps auf diesem Computer verfügbar machen",
"ai.prefs.connect_apps_note": "Trägt SolidCraft beim Start als MCP-Server in die auf diesem Computer gefundenen KI-Apps ein (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), damit ein Agent es ohne Einrichtung nutzen kann. Nur der Eintrag „solidcraft“ wird hinzugefügt oder sein Pfad aktualisiert; ein von Ihnen geänderter Eintrag bleibt unberührt, und die Datei wird vorher gesichert. Aus: Die hinzugefügten Einträge werden entfernt. Hilfe > KI-Assistenten verbinden… erledigt das von Hand.",
"ai.prefs.follow_camera": "Kamera zu Skizzen drehen",
"ai.prefs.follow_camera_off": "Die automatische Kameraführung ist aus.",
"ai.prefs.follow_camera_on": "Die Kamera dreht sich zu Skizzen, die der Agent erstellt.",
"ai.prefs.intro": "Agenten, Berechtigungen und wie ihre Arbeit angezeigt wird.",
"ai.prefs.make_default": "Standardmäßig verwenden",
"ai.prefs.no_agents": "Noch keine Agenten. Fügen Sie einen Agenten hinzu oder aktualisieren Sie, um installierte Agenten zu finden.",
"ai.prefs.no_saved_approvals": "Keine gespeicherten Freigaben.",
"ai.prefs.override_all_warning": "Dieser Agent darf das Design ohne Rückfrage speichern, überschreiben, exportieren oder ersetzen.",
"ai.prefs.permission_all": "Alles erlauben",
"ai.prefs.permission_all_summary": "Verfügbare SolidCraft-Werkzeuge werden ohne Rückfrage ausgeführt.",
"ai.prefs.permission_ask": "Vor Änderungen fragen",
"ai.prefs.permission_ask_summary": "Jede Designänderung und Dateiaktion bestätigen.",
"ai.prefs.permission_edits": "Modelländerungen erlauben",
"ai.prefs.permission_edits_summary": "Modelländerungen werden ohne Rückfrage ausgeführt; Datei- und destruktive Aktionen fragen weiterhin nach.",
"ai.prefs.permission_scope": "Gilt nur für SolidCraft-Werkzeuge. Shell und Dateiänderungen des Agenten unterliegen dessen eigenen Berechtigungen.",
"ai.prefs.pointer_preview_hidden": "Zeiger ausgeblendet",
"ai.prefs.pointer_preview_speed": "Agentenzeiger · {speed}",
"ai.prefs.pointer_speed": "Bewegungsgeschwindigkeit",
"ai.prefs.privacy_note": "Die Unterhaltung geht nur an den von Ihnen gewählten Agenten oder API-Endpunkt. API-Schlüssel werden im Schlüsselbund des Systems gespeichert.",
"ai.prefs.show_pointer": "Agentenzeiger anzeigen",
"ai.prefs.turn_limit_hint": "Höchstzahl designändernder Werkzeugaufrufe, die der Assistent für eine Nachricht ausführen darf.",
"angle, height": "Winkel, Höhe",
"appearance.dielectric_ior": "Brechungsindex des Dielektrikums",
"appearance.metallic": "Metallanteil",
@@ -552,8 +679,13 @@
"cmd.appearance_assign": "Erscheinungsbild",
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
"cmd.appearance_list": "Erscheinungsbilder auflisten",
"cmd.appearance_paint": "Aussehen aufmalen",
"cmd.appearance_pick_up": "Aussehen aufnehmen",
"cmd.appearance_replace": "Erscheinungsbild ersetzen",
"cmd.assembly_bom": "Stückliste",
"cmd.body_rename": "Körper umbenennen",
"cmd.browser_collapse_all": "Alles einklappen",
"cmd.browser_expand_all": "Alles ausklappen",
"cmd.browser_group": "Neue Gruppe",
"cmd.browser_move": "Browser-Elemente verschieben",
"cmd.browser_order": "Browser-Elemente ordnen",
@@ -565,11 +697,13 @@
"cmd.canvas_delete": "Zeichenfläche löschen",
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
"cmd.canvas_insert": "Zeichenfläche",
"cmd.canvas_to_sketch": "Skizze aus Zeichenfläche",
"cmd.component_activate": "Komponente aktivieren",
"cmd.component_break_link": "Verknüpfung trennen",
"cmd.component_capture_position": "Position erfassen",
"cmd.component_copies": "Identische Kopien finden",
"cmd.component_create": "Neue Komponente",
"cmd.component_cycle_color": "Komponentenfarbe wechseln",
"cmd.component_delete": "Komponente löschen",
"cmd.component_extract": "In Datei extrahieren",
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
@@ -634,6 +768,7 @@
"cmd.document_revision": "Konstruktionsrevision",
"cmd.document_units": "Einheiten der Konstruktion ändern",
"cmd.edit_checkpoint": "Prüfpunkt",
"cmd.edit_repeat": "Letzten Befehl wiederholen",
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
"cmd.engine_commands": "Befehle auflisten",
"cmd.engine_describe": "Befehl beschreiben",
@@ -646,6 +781,7 @@
"cmd.file_export": "Exportieren",
"cmd.file_insert_mesh": "Netz einfügen",
"cmd.file_insert_step": "STEP einfügen",
"cmd.file_open_in_easyprint": "Mit EasyPrint öffnen",
"cmd.file_save_mesh": "Als Netz speichern",
"cmd.file_slicers": "Installierte Slicer",
"cmd.frame_member": "Rahmenelement",
@@ -655,6 +791,8 @@
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
"cmd.inspect_clearance": "Freiraum",
"cmd.inspect_colored_components": "Komponentenfarben einschalten",
"cmd.inspect_colored_components_disable": "Komponentenfarben ausschalten",
"cmd.inspect_compare": "Körper vergleichen",
"cmd.inspect_continuity": "Kontinuitätsprüfung",
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
@@ -664,6 +802,7 @@
"cmd.inspect_interference": "Eingriff",
"cmd.inspect_isocurve": "Isokurvenanalyse",
"cmd.inspect_minimum_radius": "Mindestradiusanalyse",
"cmd.inspect_parameter_sweep": "Parametervariation",
"cmd.inspect_silhouette": "Silhouette",
"cmd.inspect_zebra": "Zebra-Analyse",
"cmd.joint_as_built": "Fertigungsgelenk",
@@ -708,8 +847,10 @@
"cmd.motion_plot": "Diagramm der Bewegungsuntersuchung",
"cmd.motion_study": "Bewegungsuntersuchung",
"cmd.motion_trace": "Bewegungsbahn",
"cmd.motion_trace_edit": "Bewegungsbahn bearbeiten",
"cmd.motion_trace_keep": "Bewegungsbahn behalten",
"cmd.occurrence_copy": "Einfügen (Instanz)",
"cmd.occurrence_drag": "Instanz ziehen",
"cmd.occurrence_ground": "Festsetzen",
"cmd.occurrence_move": "Instanz verschieben",
"cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz",
@@ -737,10 +878,14 @@
"cmd.plastic_rest": "Auflage",
"cmd.plastic_rules": "Kunststoffregeln auflisten",
"cmd.plastic_snap_fit": "Rastverbindung",
"cmd.render_aging_describe": "Mehrschichtige Oberflächenalterung",
"cmd.render_backends": "Render-Backends",
"cmd.render_batch": "Alle Aufnahmen rendern",
"cmd.render_camera_fit": "Renderobjekt einpassen",
"cmd.render_camera_get": "Renderkamera prüfen",
"cmd.render_cancel": "Rendering abbrechen",
"cmd.render_compare": "Bilder vergleichen",
"cmd.render_effects_describe": "Objektiv- und Kreativeffekte",
"cmd.render_export": "PNG exportieren",
"cmd.render_hdr_inspect": "HDR-Quelle untersuchen",
"cmd.render_library_appearance_apply": "Opakes Bibliotheksaussehen anwenden",
@@ -748,19 +893,30 @@
"cmd.render_library_environment_apply": "Integrierte HDR-Umgebung anwenden",
"cmd.render_library_list": "Render-Bibliothek durchsuchen",
"cmd.render_library_physical_prepare": "Prepare Physical Reference",
"cmd.render_library_preview": "Originale Oberflächentextur anzeigen",
"cmd.render_library_studio_apply": "Erstellte Studiolichter anwenden",
"cmd.render_library_studio_prepare": "Erstellte Studiolichter vorbereiten",
"cmd.render_library_surface_bind": "Save Surface Binding",
"cmd.render_library_surface_prepare": "Erstellte Oberflächenmaps vorbereiten",
"cmd.render_library_surface_unbind": "Remove Surface Binding",
"cmd.render_library_targets": "Ziele für Render-Ressourcen suchen",
"cmd.render_lights_solo": "Licht allein anzeigen",
"cmd.render_optics_bind": "Optische Einstellungen zuweisen",
"cmd.render_optics_unbind": "Zuweisung optischer Einstellungen entfernen",
"cmd.render_preflight": "Renderanfrage prüfen",
"cmd.render_preset_apply": "Studio-Voreinstellung anwenden",
"cmd.render_preset_resolve": "Studio-Voreinstellung überprüfen",
"cmd.render_quality_prepare": "Festes Sampling-Budget überprüfen",
"cmd.render_result_pin": "Angezeigtes Render-Ergebnis behalten",
"cmd.render_result_regrade": "Lineares Master-Rendering erneut farbkorrigieren",
"cmd.render_scene": "Renderszene prüfen",
"cmd.render_settings_apply": "Renderkamera anwenden",
"cmd.render_settings_capture_hdr": "Render-Einstellungen mit HDR speichern",
"cmd.render_settings_get": "Rendereinstellungen",
"cmd.render_settings_set": "Rendereinstellungen speichern",
"cmd.render_shot_duplicate": "Render-Aufnahme duplizieren",
"cmd.render_shot_remove": "Render-Aufnahme entfernen",
"cmd.render_shot_rename": "Render-Aufnahme umbenennen",
"cmd.render_stage_backdrop": "Physischen Renderhintergrund speichern",
"cmd.render_stage_clear": "Remove Render Stage",
"cmd.render_stage_ground": "Save Render Ground",
@@ -775,6 +931,7 @@
"cmd.sheet_fold": "Falten",
"cmd.sheet_hem": "Umkantung",
"cmd.sheet_manage_rules": "Blechregeln",
"cmd.sheet_path_flange": "Pfadflansch",
"cmd.sheet_perforate": "Perforieren",
"cmd.sheet_refold": "Wieder falten",
"cmd.sheet_rules": "Blechregeln auflisten",
@@ -786,6 +943,7 @@
"cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug",
"cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren",
"cmd.sketch_blend_curve": "Übergangskurve",
"cmd.sketch_cancel": "Skizze abbrechen",
"cmd.sketch_chain": "Kette",
"cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel",
"cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand",
@@ -850,8 +1008,11 @@
"cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten",
"cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)",
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
"cmd.sketch_spline_insert_point": "Splinepunkt hinzufügen",
"cmd.sketch_spun_profile": "Gedrehtes Profil",
"cmd.sketch_toggle_diameter": "Radius / Durchmesser umschalten",
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
"cmd.sketch_transform": "Transformieren",
"cmd.sketch_trim_path": "Entlang Pfad trimmen",
"cmd.solid_align": "Ausrichten",
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
@@ -897,13 +1058,21 @@
"cmd.timeline_suppress": "Merkmal unterdrücken",
"cmd.view_ambient_occlusion": "Umgebungsverdeckung",
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
"cmd.view_back": "Rückansicht",
"cmd.view_bottom": "Untersicht",
"cmd.view_delete": "Benannte Ansicht löschen",
"cmd.view_front": "Vorderansicht",
"cmd.view_graphics_preset": "Grafikprofil",
"cmd.view_isometric": "Isometrische Ansicht",
"cmd.view_left": "Linke Seitenansicht",
"cmd.view_list": "Benannte Ansichten auflisten",
"cmd.view_navigate": "Ansicht navigieren",
"cmd.view_normal_to": "Senkrecht zur Auswahl",
"cmd.view_rename": "Benannte Ansicht umbenennen",
"cmd.view_right": "Rechte Seitenansicht",
"cmd.view_save": "Neue benannte Ansicht",
"cmd.view_shots_strip": "Aufnahmenleiste",
"cmd.view_top": "Draufsicht",
"color.copy": "Klicken, um Farbwerte zu kopieren",
"color.edit": "Klicken, um Farbe zu bearbeiten",
"color.gamma": "Farbwerte im Gammaraum 0-255 anzeigen",
@@ -944,6 +1113,7 @@
"crash.report": "Bericht…",
"crash.unclean_explanation": "Beim späteren Start erkannt; der genaue Zeitpunkt des Beendens wurde nicht aufgezeichnet. Auch ein Herunterfahren des Systems oder ein erzwungener Stopp kann ein unsauberes Beenden verursachen.",
"delete.and_more": "… und {count} weitere",
"diagnostic.empty_boolean_component": "In Komponente '{component}' gibt es keinen Körper zum Abziehen oder für eine Schnittmenge; das Werkzeug überschneidet sich eventuell mit '{body}' in Komponente '{other}' – aktivieren Sie diese Komponente oder wählen Sie den Körper als Ziel",
"dialog.bodies_count": "{count} Körper",
"dialog.clearance_invalid": "der Abstand muss eine Länge von 0 oder mehr sein",
"dialog.compute": "Berechnen",
@@ -959,7 +1129,18 @@
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
"dialog.selection_step": "{step} von {total}: {input} auswählen",
"dialog.step": "Schritt",
"document.changed_on_disk": "{name} wurde auf dem Datenträger geändert",
"document.changed_on_disk_detail": "{name} wurde seit dem Öffnen oder letzten Speichern von einem anderen Programm oder Computer geändert. „Überschreiben“ ersetzt diese Änderungen durch Ihre; „Kopie speichern“ behält beide.",
"document.copy_name": "{name} - Kopie",
"document.open_anyway": "Trotzdem öffnen",
"document.open_elsewhere": "{name} ist bereits geöffnet",
"document.open_elsewhere_detail": "{name} ist in SolidCraft {app} auf {who} geöffnet, seit {when}. Wenn Sie beide speichern, verliert einer von Ihnen Arbeit. Öffnen Sie die Datei schreibgeschützt, um sie anzusehen, ohne sie zu überschreiben.",
"document.open_read_only": "Schreibgeschützt öffnen",
"document.opened_read_only": "Schreibgeschützt geöffnet: Speichern fragt nach einer neuen Datei",
"document.overwrite": "Überschreiben",
"document.read_only_save": "Dieses Design wurde schreibgeschützt geöffnet: Speichern Sie es als neue Datei",
"document.save_changes": "Änderungen in {name} speichern?",
"document.save_copy": "Kopie speichern…",
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
"drive.stopped_by_contact": "Angehalten bei {value}: {a} würde {b} durchdringen",
"drive.stopped_by_loop": "Angehalten bei {value}: die Schleife über {joint} lässt sich nicht weiter schließen",
@@ -976,9 +1157,11 @@
"dxf.progress.write": "DXF schreiben (5/5)",
"dxf.tolerance": "Kurventoleranz",
"dxf.tolerance_help": "Nur sichtbare Körper und angezeigte Skizzen. Kurven werden durch gerade Segmente angenähert. Eine kleinere Toleranz liefert feinere Kurven und benötigt mehr Zeit und Speicher.",
"file.open.drawing_filter": "Designs, CAD-Dateien, Netze und Zeichnungen",
"file_load.built": "Bisher erstellt: {built} von {total} Merkmalen.",
"file_load.converting": "Volumenkörper {done} von {total} wird konvertiert",
"file_load.inserting": "Geometrie wird eingefügt",
"file_load.link_item": "{name} ({done} von {total})",
"file_load.opening": "Design wird geöffnet",
"file_load.preparing_thumbnail": "Vorschaubild wird vorbereitet...",
"file_load.preparing_view": "Ansicht wird vorbereitet...",
@@ -988,17 +1171,33 @@
"file_load.rebuilding": "Merkmal {done} von {total} wird neu aufgebaut",
"file_load.recovering": "Design wird wiederhergestellt",
"file_load.stopping": "Warten, bis der vorherige Vorgang beendet ist...",
"file_load.updating_links": "Verknüpfungen werden aktualisiert",
"file_load.waiting": "Warten auf Dateivorgang",
"fits the face, e.g. M8": "passt zur Fläche, z. B. M8",
"from the profile": "aus dem Profil",
"from the rule": "aus der Regel",
"help.apache": "Apache-Lizenz 2.0",
"help.attributions": "Quellenangaben (ATTRIBUTION.md)",
"help.build": "Version {version} · Commit {commit} · erstellt am {date}",
"help.connect_note": "Starten Sie jeden Assistenten neu, damit er den SolidCraft-Server lädt. Jede Datei wurde vor der Änderung als .bak gesichert.",
"help.connect_title": "KI-Assistenten verbinden",
"help.font_details": "Die Oberfläche verwendet die integrierten Schriftarten von egui (Ubuntu Light und Hack unter der Ubuntu Font Licence und der MIT/Bitstream-Vera-Lizenz; Noto Emoji und die Emoji-Symbolschrift unter OFL und MIT). Skizzentext verwendet Liberation Sans (SIL OFL 1.1). Das optionale Schriftpaket craft-fonts von The Crafting Apps (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1) ist nicht in SolidCraft-Builds enthalten.",
"help.fonts": "Schriftarten",
"help.licences": "Lizenzen",
"help.mit": "MIT-Lizenz",
"help.notice": "NOTICE",
"help.solid.chamfer": "Schneidet die gewählten Kanten mit einer ebenen Fase im eingestellten Abstand ab. Damit brechen Sie scharfe Kanten oder erleichtern das Einführen eines Stifts oder einer Schraube.",
"help.solid.combine": "Kombiniert Körper: vereinigt sie, zieht sie von einem Zielkörper ab oder behält nur das gemeinsame Volumen; die Werkzeugkörper können erhalten bleiben. Damit fügen Sie Teile zusammen, schneiden eine Form mit einer anderen aus oder finden, wo sich zwei Körper überschneiden.",
"help.solid.extrude": "Zieht ein Skizzenprofil über eine Strecke in die dritte Dimension und fügt dabei Material hinzu oder entfernt es. Damit machen Sie aus einer flachen Kontur einen Block, eine Erhebung oder eine Tasche.",
"help.solid.fillet": "Rundet die gewählten Kanten mit einem glatten Kreisübergang im eingestellten Radius ab. Damit entschärfen Sie scharfe Kanten für Festigkeit, Griffigkeit oder Optik.",
"help.solid.hole": "Bohrt an einem gewählten Punkt auf einer Fläche ein rundes Loch im eingestellten Durchmesser. Für Durchgangsbohrungen für Schrauben, Stifte und andere einfache runde Öffnungen.",
"help.solid.loft": "Erzeugt einen glatten Übergangskörper zwischen zwei oder mehr Skizzenprofilen oder von einem Profil zu einem Punkt. Für Übergänge, Trichter, Griffe und alle Formen, deren Querschnitt sich über die Länge ändert.",
"help.solid.mirror": "Erzeugt eine gespiegelte Kopie der gewählten Körper oder Features an einer Ebene und kann sie mit dem Original vereinigen. Für symmetrische Teile: Sie modellieren eine Hälfte und erhalten beide.",
"help.solid.pattern.circular": "Wiederholt die gewählten Objekte um eine Achse, mit der eingestellten Anzahl und dem Gesamtwinkel. Für Lochkreise, Speichen, Kühlrippen und Zähne.",
"help.solid.pattern.rectangular": "Wiederholt die gewählten Objekte in einer oder zwei Richtungen mit der eingestellten Anzahl und dem Abstand. Für Reihen und Raster von Bohrungen, Rippen, Stiften oder Füßen.",
"help.solid.revolve": "Dreht ein Skizzenprofil um eine Achse und erzeugt so einen runden Körper. Für Wellen, Knöpfe, Schalen und alle Teile, die um eine Mittellinie rund sind.",
"help.solid.shell": "Höhlt einen Körper aus, lässt Wände in der eingestellten Dicke stehen und kann gewählte Flächen öffnen. Für Kästen, Deckel und andere dünnwandige Teile.",
"help.solid.sweep": "Führt ein Skizzenprofil entlang eines Pfads und erzeugt so einen Körper. Für Rohre, Schienen, Griffe und alle Teile mit gleichbleibendem Querschnitt entlang einer Kurve.",
"help.third_party": "Drittanbieterlizenzen",
"home.back": "Zurück zu {name}",
"home.crashes": "Absturzberichte",
@@ -1030,7 +1229,7 @@
"home.new_mm": "Neuer Entwurf",
"home.new_mm_hint": "Millimeter · öffnet sich im Arbeitsbereich Entwurf",
"home.open": "Öffnen…",
"home.open_hint": "Ein .solidcraft-Entwurf ({shortcut})",
"home.open_hint": "Ein .sc-Entwurf ({shortcut})",
"home.open_now": "Jetzt öffnen",
"home.opened_days_ago": "Vor {count} Tagen",
"home.opened_hours_ago": "Vor {count} Std.",
@@ -1064,6 +1263,9 @@
"install.steps_intro": "Safari installiert Web-Apps über seine eigenen Menüs:",
"install.title": "SolidCraft installieren",
"issue.body_label": "Berichtstext",
"issue.note_changed_while_waiting": "Einstellungen oder Modell haben sich geändert, während auf die Vorschau gewartet wurde. Nehmen Sie den Screenshot neu auf, um die technischen Details für die aktuelle Ansicht zu aktualisieren.",
"issue.note_details_kept": "Screenshot lokal aufgenommen. Ihre bearbeiteten technischen Details wurden beibehalten: Prüfen Sie, ob sie noch zur aktuellen Ansicht passen.",
"issue.note_details_updated": "Technische Details an die aktuelle Ansicht angepasst. Screenshot lokal aufgenommen. Prüfen Sie ihn, bevor Sie ihn auf GitHub anhängen.",
"issue.note_image_copied": "Fügen Sie das Bild in den GitHub-Entwurf ein oder speichern Sie das PNG, falls das Kopieren von Bildern nicht verfügbar ist.",
"issue.note_opened": "Prüfen Sie den GitHub-Entwurf und hängen Sie den Screenshot an. Die Schaltfläche Submit new issue von GitHub veröffentlicht ihn.",
"issue.note_opened_copied": "Bericht kopiert. Fügen Sie ihn in den GitHub-Entwurf ein, hängen Sie den Screenshot an und klicken Sie auf GitHub auf Submit new issue.",
@@ -1075,6 +1277,8 @@
"joint.from_bodies_hint": "Jeder ausgewählte Körper kommt in eine eigene neue Komponente und bleibt, wo er ist.",
"joint.one_body": "Beide Auswahlen liegen auf {body}. Wählen Sie die zweite auf einem anderen Teil.",
"joint.one_component": "Diese Teile sind Körper einer Komponente: {bodies}. Ein Gelenk bewegt eine Komponente gegenüber einer anderen, daher braucht jedes Teil eine eigene Komponente.",
"joint.rigid_group_one_component": "Diese {count} Körper liegen alle in einer Komponente (oder direkt im Design): Verwenden Sie zuerst {command}.",
"joint.rigid_group_select_components": "Wählen Sie zwei oder mehr Komponenten aus.",
"keymap.command_id": "Befehls-ID",
"keymap.empty": "Keine passenden Befehle",
"keymap.use_anyway": "{key} hier trotzdem verwenden",
@@ -1085,8 +1289,51 @@
"manage.material_hint": "Wählen Sie Körper und deren physikalisches Material aus.",
"manage.parameters_hint": "Bearbeiten Sie Ausdrücke und Einheiten. Änderungen werden mit der Eingabetaste oder beim Verlassen eines Feldes übernommen.",
"manage.rules_hint": "Wählen Sie eine Regel zum Bearbeiten oder erstellen Sie eine neue. Bestätigen Sie, um die Regel zu speichern.",
"material.editor.add_property": "Eigenschaft hinzufügen",
"material.editor.add_source": "Quelle hinzufügen",
"material.editor.assigned_density": "Zugewiesene Dichte",
"material.editor.axes": "Materialachsen",
"material.editor.axes_numeric": "1 / 2 / 3",
"material.editor.axes_wood": "L / R / T",
"material.editor.copy_name": "Kopie von {name}",
"material.editor.density_range": "Die zugewiesene Dichte muss größer als 0 und höchstens 1000 g/cm³ sein.",
"material.editor.designation": "Bezeichnung",
"material.editor.duplicate": "Duplizieren",
"material.editor.engineering_card": "Technisches Datenblatt",
"material.editor.family": "Werkstoffgruppe",
"material.editor.glass": "Glas",
"material.editor.illustrative": "Beispielwerte",
"material.editor.invalid_property": "Nicht unterstützter Eigenschaftstyp.",
"material.editor.isotropic": "Isotrop",
"material.editor.measurement": "Messung",
"material.editor.metal": "Metall",
"material.editor.moisture": "Feuchte, bezogen auf Trockenmasse",
"material.editor.name_required": "Geben Sie einen Namen mit 1–128 Zeichen ein.",
"material.editor.no_draft": "Kein Materialentwurf geöffnet.",
"material.editor.number_required": "Geben Sie einen gültigen Zahlenwert ein.",
"material.editor.orthotropic": "Orthotrop",
"material.editor.other": "Sonstiges",
"material.editor.polymer": "Polymer",
"material.editor.preserved_appearance": "Beibehaltenes Erscheinungsbild",
"material.editor.public_reference": "Öffentliche Quelle",
"material.editor.reference": "Referenz",
"material.editor.save_or_discard": "Speichern oder verwerfen Sie den Materialentwurf, bevor Sie ein Material zuweisen.",
"material.editor.saved": "Material gespeichert. Körperzuweisungen bleiben unverändert.",
"material.editor.scope": "Speichern legt dieses Material ab, ohne es Körpern zuzuweisen. Leere Eigenschaften und Bedingungen bleiben unbekannt. Die Dichte im Datenblatt ändert weder die zugewiesene Dichte noch die Masse.",
"material.editor.source_id": "Quellen-ID",
"material.editor.source_kind": "Quellentyp",
"material.editor.source_title": "Quellentitel",
"material.editor.sources": "Quellen",
"material.editor.stale": "Dieses Material wurde geändert, während der Entwurf geöffnet war. Verwerfen und öffnen Sie es erneut, bevor Sie speichern.",
"material.editor.state": "Materialzustand",
"material.editor.strain_rate": "Dehnrate",
"material.editor.symmetry": "Elastische Symmetrie",
"material.editor.temperature": "Temperatur",
"material.editor.unknown_card": "Die technischen Eigenschaften sind unbekannt. Beim Speichern bleiben eventuell nur die zugewiesene Dichte und das vorhandene Erscheinungsbild erhalten.",
"material.editor.user_supplied": "Vom Benutzer angegeben",
"material.editor.wood": "Holz",
"material.selection.choose": "Material auswählen",
"material.selection.mixed": "Unterschiedliche Materialien",
"measure.at": "Bei",
"measure.item": "Element {number}",
"messages.ago_h": "vor {n} Std.",
@@ -1115,6 +1362,9 @@
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
"nav.free_orbit.label": "Freie Drehung",
"nav.free_orbit.snap_help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
"nav.free_orbit_upright.help": "Seitliches Ziehen mit der Maus dreht das Modell um die senkrechte Achse der Welt; Ziehen nach oben und unten neigt es ohne Begrenzung über den Pol hinweg.",
"nav.free_orbit_upright.label": "Um die senkrechte Achse drehen",
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
@@ -1143,8 +1393,28 @@
"pattern.flip_direction": "Richtung umkehren",
"pick a hole to seat it, or OK places it at the origin": "Wählen Sie eine Bohrung zum Einsetzen, oder OK platziert es im Ursprung",
"pick one or two vertices, edges or faces": "Wählen Sie ein oder zwei Ecken, Kanten oder Flächen",
"prefs.bodies_folders": "Körperordner",
"prefs.bodies_folders_always": "Immer",
"prefs.bodies_folders_never": "Nie",
"prefs.bodies_folders_several": "Nur bei mehreren Körpern",
"prefs.cpu": "CPU (Software)",
"prefs.dialogs": "Dialoge",
"prefs.dialogs_edge": "Am Rand der Ansicht",
"prefs.dialogs_left_column": "In der linken Spalte",
"prefs.dialogs_note": "Wo Befehlsdialoge öffnen: am Rand der Ansicht angedockt oder in der linken Spalte anstelle von Browser und Verlauf, die beim Schließen des Dialogs wie zuvor zurückkehren.",
"prefs.history_origin_bodies": "Ursprung und Körper oben zeigen",
"prefs.history_origin_bodies_note": "Der Verlauf links oder unter dem Browser beginnt mit dem Körper-Ordner der aktiven Komponente und dem Ursprung, mit Augen und Menüs wie im Browser. Der Verlauf unten bleibt unverändert.",
"prefs.minutes_suffix": " min",
"prefs.origin_plane_names": "Namen der Ursprungsebenen",
"prefs.origin_plane_names_note": "Namen in Browser, Dialogen und Listen. {top} ist XY, {front} ist XZ und {right} ist YZ, mit Z nach oben. Befehle, Skripte und gespeicherte Konstruktionen behalten XY, XZ und YZ.",
"prefs.search": "Einstellungen suchen",
"prefs.search_empty": "Keine passenden Einstellungen",
"prefs.theme_follow": "Einer Designdatei folgen",
"prefs.theme_follow_error": "Nicht angewendet: {error}. Das letzte gültige Design bleibt aktiv.",
"prefs.theme_follow_launch": "Nur für diesen Start (--theme-file); wird neu geladen, sobald ein anderes Werkzeug die Datei überschreibt.",
"prefs.theme_follow_note": "Wird neu geladen, sobald ein anderes Werkzeug die Datei überschreibt. Die Auswahl eines Designs beendet das Folgen.",
"prefs.theme_follow_off": "Aus",
"prefs.theme_follow_stop": "Nicht mehr folgen",
"properties.title": "Eigenschaften: {name}",
"recovery.days_ago": "vor {count} Tagen",
"recovery.detail": "{file} · {features} Merkmale · automatisch gespeichert {when}",
@@ -1153,6 +1423,7 @@
"recovery.min_ago": "vor {count} Min.",
"recovery.never_saved": "nie gespeichert",
"render.add_ground": "Add fitted ground to setup",
"render.all_shots_label": "Alle Aufnahmen",
"render.background": "Szenenhintergrund",
"render.bloom_enable": "Darstellungs-Bloom",
"render.bloom_help": "Kreativer Ausgabeglanz aus linearen Glanzlichtern mit drei normierten Durchläufen. Radius in Ausgabepixeln; Addition vor sRGB. Keine Linsen-, Volumen- oder Konvergenzsimulation. Das rohe EXR bleibt unverändert.",
@@ -1174,8 +1445,17 @@
"render.cpu_budget_studio": "Studio (große Bilder)",
"render.cpu_budget_triangles": "Dreiecksobergrenze",
"render.cpu_budget_workstation": "Workstation (komplexe Szenen)",
"render.export_image_label": "Bild exportieren",
"render.exr_pending": "Die GPU kann rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
"render.fit": "Produkt einpassen",
"render.fixed_camera": "Kamera",
"render.fixed_world": "Welt",
"render.heading_batch": "Stapel",
"render.heading_frame": "Bildrahmen",
"render.heading_lens": "Objektiv",
"render.heading_results": "Ergebnisse",
"render.heading_shots": "Aufnahmen",
"render.hide_shots_label": "Aufnahmenleiste ausblenden",
"render.image_cancelled": "Rendering abgebrochen",
"render.image_cancelling": "Wird abgebrochen",
"render.image_completed": "Bild bereit zum Export",
@@ -1199,6 +1479,7 @@
"render.image_use_largest": "Größte Größe mit diesem Seitenverhältnis verwenden:",
"render.image_width": "Breite",
"render.include_rig_cards": "Include passive cards and flags",
"render.inspector": "Inspektor",
"render.library_applied": "Ressourcenvariante angewendet. Vollständigen Renderauftrag vor dem Start prüfen.",
"render.library_apply_hdr": "Mit dieser HDR-Umgebung speichern",
"render.library_apply_maps": "Erstellte GPU-Oberflächenmaps verwenden",
@@ -1234,6 +1515,8 @@
"render.library_target": "Zielkörper",
"render.library_title": "Render-Ressourcenbibliothek",
"render.library_uniform_ack": "Einheitliche Näherung ohne räumliche oder erweiterte Effekte verwenden",
"render.light_anchor_pending": "Das Koordinatensystem der Lichter kann noch nicht geändert werden.",
"render.lights_fixed_to": "Lichter gebunden an",
"render.material_color_replace": "Basismap ersetzt Körperfarbe",
"render.material_limits": "Höchstens 128 Körperüberschreibungen, 32 getrennte Glaskörper, 64 Beschichtungen und 128 Texturbindungen. Identische Maps teilen Speicher; Geräte- und Gesamtspeichergrenzen bleiben aktiv.",
"render.material_metallic_map": "Metallanteil-Map (linear)",
@@ -1243,6 +1526,8 @@
"render.opaque_image_summary": "Vorschau-PNG · opakes PBR · Transparenz durch Blending",
"render.opaque_preview_help": "Opakes PBR und einfache Farben; Transparenz durch Blending. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Filtern. Export ersetzt nie durch Vorschaucache oder anderen Auftrag.",
"render.output_tools_inspector": "Größe und Format im Ausgabe-Inspektor bearbeiten.",
"render.paint_label": "Aufmalen",
"render.passive_stage": "Render-only stage",
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
"render.perspective": "Perspektivische Kamera",
@@ -1255,9 +1540,16 @@
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
"render.pick_component": "Komponente",
"render.pick_label": "Auswahl",
"render.pick_up_label": "Aufnehmen",
"render.pick_waiting": "Die Produktauswahl wird noch nicht unterstützt.",
"render.preview": "Integrierte Vorschau",
"render.preview_help": "Rasterdarstellung mit gemischter Transparenz. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
"render.preview_lighting_help": "Die Live-Vorschau zeigt nur direktes Licht und keine Schatten.",
"render.regrade_label": "Neu abstimmen",
"render.reload_settings": "Gespeicherte Einstellungen laden",
"render.replace_label": "Ersetzen",
"render.replace_saved_surface": "Replace an existing saved binding for this target",
"render.restore_camera": "Gespeicherte Kamera laden",
"render.result_reduced_display": "Anzeigevorschau auf die Geräte-/Cachegrenze reduziert. PNG und Export behalten die volle Auflösung; native 100%-Ansicht ist hier nicht verfügbar.",
@@ -1284,15 +1576,33 @@
"render.ribbon_studios_help": "Implementierte Studios durchsuchen und Ersetzungen vor dem Anwenden prüfen. Konzepte bleiben gekennzeichnet.",
"render.ribbon_surface_maps": "Oberflächen",
"render.save_setup": "Setup speichern",
"render.save_shot": "Aufnahme speichern",
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
"render.saved_surface_remove": "Remove saved binding",
"render.saved_surfaces": "Saved surface bindings",
"render.saved_surfaces_help": "Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
"render.setup_name": "Name des Setups",
"render.shell_tab_appearance": "Aussehen",
"render.shell_tab_camera": "Kamera",
"render.shell_tab_scene": "SZENE",
"render.show_shots_label": "Aufnahmenleiste einblenden",
"render.stage_frame": "Bildausschnitt",
"render.stage_light": "Licht",
"render.stage_look": "Aussehen",
"render.stage_orthographic": "Orthografisch",
"render.stage_output": "Ausgabe",
"render.stage_panel_count": "Saved panels",
"render.stage_preview_note": "Integrierte Vorschau · direktes Licht",
"render.strip_label": "Leiste",
"render.unavailable_preview": "Dieses Setup verlangt ein nicht verfügbares Vorschauprofil.",
"render.unsaved": "Ungespeicherte Vorschauänderungen",
"render.use_preview": "Integrierte Vorschau verwenden",
"render.wait_batch": "Noch nicht unterstützt: Renderwarteschlange gespeicherter Aufnahmen",
"render.wait_compare": "Noch nicht unterstützt: Vergleich gespeicherter Bilder",
"render.wait_paint": "Noch nicht unterstützt: Produktauswahl und Auftragen von Erscheinungsbildern",
"render.wait_pick_up": "Noch nicht unterstützt: Produktauswahl und Übernehmen von Erscheinungsbildern",
"render.wait_replace": "Noch nicht unterstützt: Ersetzen aller Verwendungen eines Erscheinungsbilds",
"render.wait_solo": "Noch nicht unterstützt: Lichtsichtbarkeit und Solo-Steuerung",
"sample.bracket.about": "Grundflansch mit Kantenflanschen und einer Umkantung nach einer 2-mm-Blechregel",
"sample.bracket.name": "Blechwinkel",
"sample.enclosure.about": "Ausgehöhlter Block mit Lippe, Schraubenerhöhungen, Auflage und Rastverbindung",
@@ -1366,12 +1676,14 @@
"thread.designation": "Bezeichnung",
"thread.iso_metric": "ISO metrisch",
"thread.select_size": "Größe wählen",
"timeline.delete_folders_with_features_tip": "Löscht die Ordner mit allen Features darin und ihren Unterordnern",
"timeline.feature_hint": "Doppelklicken zum Bearbeiten · Ziehen zum Neuordnen · Rechtsklicken für weitere Optionen",
"timeline.group": "Neuen Merkmalsordner anlegen",
"timeline.move_many": "Zeitleistenelemente verschieben",
"timeline.move_to_group": "Merkmale in Ordner verschieben",
"timeline.rename_group": "Merkmalsordner umbenennen",
"timeline.reorder_many": "Merkmale neu ordnen",
"timeline.suppress_folders_tip": "Unterdrückt alle Features in den Ordnern und ihren Unterordnern oder hebt die Unterdrückung auf",
"timeline.ungroup": "Merkmalsordner entfernen",
"to the face": "bis zur Fläche",
"to the next face": "bis zur nächsten Fläche",
@@ -1382,7 +1694,11 @@
"toolbar.render_camera": "KAMERA",
"toolbar.render_environment": "UMGEBUNG",
"toolbar.render_lighting": "BELEUCHTUNG",
"toolbar.render_lights": "LICHTER",
"toolbar.render_result": "ERGEBNIS",
"toolbar.render_scene": "SZENE",
"toolbar.render_stage": "BÜHNE",
"toolbar.render_studio": "STUDIO",
"trace.points": "{count} Punkte entlang der Bahn",
"trace.stopped_by_contact": "Endet bei Probe {sample}: {a} würde {b} durchdringen",
"trace.stopped_by_loop": "Endet bei Probe {sample}: die Schleife über {joint} lässt sich nicht weiter schließen",
@@ -1580,7 +1896,7 @@
"ui.agent_chat_pop_out": "In eigenem Fenster öffnen, das Sie auf einen anderen Bildschirm verschieben können",
"ui.agent_chat_previous_picture": "Vorheriges Bild",
"ui.agent_chat_privacy": "Nichts geht irgendwohin außer an den Agenten, den Sie unter Ihrem eigenen Konto ausführen.",
"ui.agent_chat_privacy_note": "SolidCraft sendet nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
"ui.agent_chat_privacy_note": "Der Chat sendet von sich aus nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
"ui.agent_chat_queue": "Einreihen",
"ui.agent_chat_queue_full": "Zu viele Nachrichten warten: Entfernen Sie eine oder warten Sie auf den Agenten.",
"ui.agent_chat_queued": "Eingereiht",
@@ -1765,6 +2081,7 @@
"ui.command_done": "{command} fertig",
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
"ui.concentric": "Konzentrisch",
"ui.cone": "Kegel",
"ui.conic_curve": "Kegelschnitt",
"ui.constant": "Konstant",
"ui.constraints_conflict": "Bedingungen konfliktreich",
@@ -1823,6 +2140,11 @@
"ui.draft": "Abziehen",
"ui.drawing": "Zeichnung",
"ui.drill_point": "Bohrpunkt",
"ui.easyprint_consent": "EasyPrint ist der Slicer von Printables im Browser. Um das Modell dort zu öffnen, lädt SolidCraft es auf die Server von Printables hoch. Der Link läuft nach 30 Minuten ab, und Sie müssen bei Printables angemeldet sein. Fortfahren?",
"ui.easyprint_failed": "Upload zu EasyPrint fehlgeschlagen: {error}",
"ui.easyprint_opened": "In EasyPrint geöffnet",
"ui.easyprint_row": "EasyPrint (Printables, im Browser)",
"ui.easyprint_uploading": "Wird zu Printables hochgeladen …",
"ui.edit": "Bearbeiten",
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
@@ -1864,6 +2186,7 @@
"ui.fully_constrained": "Vollständig bestimmt",
"ui.function": "Funktion",
"ui.general": "Allgemein",
"ui.gpu_memory_lowered": "Die Grafikkarte hat keinen Speicher mehr frei: Die Ansicht wird in geringerer Qualität gezeichnet.",
"ui.graphics": "Grafik",
"ui.graphics_custom": "Benutzerdefiniert",
"ui.graphics_performance": "Leistung",
@@ -1882,6 +2205,7 @@
"ui.heading_circular_pattern": "RADIALES MUSTER",
"ui.heading_coil": "SCHRAUBE",
"ui.heading_combine": "KOMBINIEREN",
"ui.heading_cone": "KEGEL",
"ui.heading_configure": "KONFIGURIEREN",
"ui.heading_constraints": "RANDBEDINGUNGEN",
"ui.heading_construct": "KONSTRUKTION",
@@ -2105,6 +2429,7 @@
"ui.render": "Rendern",
"ui.renderer": "Renderer",
"ui.repeat": "Wiederholen",
"ui.repeat_nothing": "Noch nichts zu wiederholen: Starten Sie zuerst ein Werkzeug oder einen Dialog.",
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
"ui.report_a_bug": "Fehler melden",
"ui.reset": "Zurücksetzen",
@@ -2123,9 +2448,7 @@
"ui.right": "Rechts",
"ui.save": "Speichern",
"ui.save_as": "Speichern unter",
"ui.save_mesh_no_slicer": "Kein Slicer gefunden: „Benutzerdefiniert…“ wählen und das Slicer-Programm angeben, oder „An Slicer senden“ ausschalten",
"ui.save_mesh_pick_bodies": "Wähle die Körper zum Speichern aus",
"ui.save_mesh_temp_failed": "{folder} konnte nicht angelegt werden: {error}",
"ui.saved_to": "Gespeichert: {path}",
"ui.sbox_pin": "Oben im S-Feld anheften",
"ui.sbox_unpin": "Lösen",
@@ -2140,7 +2463,6 @@
"ui.selection_shift_add_only": "Umschalt+Klick fügt hinzu, ohne zu entfernen",
"ui.selection_shift_add_only_tip": "An: Umschalt fügt Elemente hinzu, Strg/Cmd schaltet um. Aus (Standard): Umschalt und Strg/Cmd schalten beide um.",
"ui.send": "Senden",
"ui.send_to_slicer": "An Slicer senden",
"ui.sent_to_slicer": "An {slicer} gesendet",
"ui.sheet_gap": "Spalt",
"ui.shell": "Hülle",
@@ -2238,6 +2560,7 @@
"ui.untitled": "Ohne Titel",
"ui.up_axis_note": "Welche Achse in den Ansichten nach oben zeigt (Achsenansicht, Home, Ansehen). Designs bleiben unverändert.",
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
"ui.upload": "Hochladen",
"ui.view_centre": "Ansichtszentrum",
"ui.viewport_theme_follow": "Dem Design folgen",
"ui.visual_shaded_hidden_edges": "Schattiert mit verdeckten Kanten",
@@ -2300,5 +2623,6 @@
"update.up_to_date": "SolidCraft ist aktuell.",
"update.update_now": "Jetzt aktualisieren",
"update.you_have": "Installiert ist {version}.",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ auswählen · Eingabe / Tab vervollständigen · Esc schließen"
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ auswählen · Eingabe / Tab vervollständigen · Esc schließen",
"⌘Enter continues on GitHub.": "⌘Enter geht weiter zu GitHub."
}
+7
View File
@@ -1,4 +1,5 @@
{
"Base Center": "Base Centre",
"Center X": "Centre X",
"Center Y": "Centre Y",
"Center of mass": "Centre of mass",
@@ -16,6 +17,12 @@
"cmd.sketch_slot_center_point": "Centre Point Slot",
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"prefs.theme_follow": "Follow a theme file",
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
"prefs.theme_follow_off": "Off",
"prefs.theme_follow_stop": "Stop following",
"render.aging_add": "Add aging layer",
"render.aging_airborne": "Airborne dust / participating medium",
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
+158 -6
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "Add Mid Radius",
"Add Row": "Add Row",
"Add a tool": "Add a tool",
"Add all bodies": "Add all bodies",
"Add bend": "Add bend",
"Add condition": "Add condition",
"Add or edit rule": "Add or edit rule",
"Add to New Folder": "Add to New Folder",
"Add visible bodies": "Add visible bodies",
"Advanced": "Advanced",
"Against": "Against",
"Alignment": "Alignment",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
"BROWSER": "BROWSER",
"Bar": "Bar",
"Base Center": "Base Center",
"Bend": "Bend",
"Bend Edges": "Bend Edges",
"Bend Line Position": "Bend Line Position",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "Create Issue — review draft",
"Create Issue…": "Create Issue…",
"Cross curves": "Cross curves",
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter continues on GitHub.",
"Cube": "Cube",
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
"Current perspective view": "Current perspective view",
@@ -152,6 +156,7 @@
"Customize…": "Customize…",
"Cylindrical face": "Cylindrical face",
"Delete": "Delete",
"Delete Folders and Features…": "Delete Folders and Features…",
"Delete Selected Features…": "Delete Selected Features…",
"Delete Selected Folders": "Delete Selected Folders",
"Delete all": "Delete all",
@@ -430,6 +435,9 @@
"Remove this point": "Remove this point",
"Rename Feature": "Rename Feature",
"Rename Folder": "Rename Folder",
"Render Stage": "Render Stage",
"Reopen designs": "Reopen designs",
"Reopen the designs that were open when SolidCraft was last closed": "Reopen the designs that were open when SolidCraft was last closed",
"Replace existing library file": "Replace existing library file",
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
"Report text copied. Paste it into GitHub and submit your report there.": "Report text copied. Paste it into GitHub and submit your report there.",
@@ -532,6 +540,7 @@
"Style": "Style",
"Summary": "Summary",
"Suppress": "Suppress",
"Suppress Selected Folders": "Suppress Selected Folders",
"Suppressed": "Suppressed",
"Surface": "Surface",
"Surface, or faces to turn over": "Surface, or faces to turn over",
@@ -579,6 +588,7 @@
"UNTRIM": "UNTRIM",
"Unit": "Unit",
"Unsuppress": "Unsuppress",
"Unsuppress Selected Folders": "Unsuppress Selected Folders",
"Updating the preview": "Updating the preview",
"Use active author placement": "Use active author placement",
"Use fixed plane": "Use fixed plane",
@@ -626,6 +636,8 @@
"ai.prefs.background_commands": "Run agent commands in the background",
"ai.prefs.connect_apps": "Make SolidCraft available to AI apps on this computer",
"ai.prefs.connect_apps_note": "When SolidCraft starts, adds it as an MCP server to the AI apps found on this computer (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), so an agent can use it without any setup. Only the solidcraft entry is added or its path refreshed; an entry you changed is left alone, and the file is backed up first. Off: the entries it added are removed. Help > Connect AI assistants… does this by hand.",
"ai.prefs.connect_at_start": "Connect the last-used agent when SolidCraft starts",
"ai.prefs.connect_at_start_note": "Starts the agent you connected last (or the default one) in the background each time SolidCraft starts, so it is ready for chatting. Not when SolidCraft is started with --control. If it cannot connect, the Assistant shows Connection failed and a Retry button.",
"ai.prefs.follow_camera": "Turn the camera to sketches",
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
@@ -633,6 +645,8 @@
"ai.prefs.make_default": "Use by default",
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
"ai.prefs.no_saved_approvals": "No saved approvals.",
"ai.prefs.open_at_start": "Open the Assistant panel when SolidCraft starts",
"ai.prefs.open_at_start_note": "Shows the Assistant panel (or its own window) each time SolidCraft starts. Not when SolidCraft is started with --control.",
"ai.prefs.override_all_warning": "This agent may save, overwrite, export or replace the design without asking.",
"ai.prefs.permission_all": "Allow everything",
"ai.prefs.permission_all_summary": "Available SolidCraft tools run without asking.",
@@ -677,6 +691,9 @@
"cmd.appearance_assign": "Appearance",
"cmd.appearance_library": "Appearance Library",
"cmd.appearance_list": "List Appearances",
"cmd.appearance_paint": "Paint Appearance",
"cmd.appearance_pick_up": "Pick Up Appearance",
"cmd.appearance_replace": "Replace Appearance",
"cmd.assembly_bom": "Bill of Materials",
"cmd.body_rename": "Rename Body",
"cmd.browser_collapse_all": "Collapse All",
@@ -692,10 +709,12 @@
"cmd.canvas_delete": "Delete Canvas",
"cmd.canvas_edit": "Edit Canvas",
"cmd.canvas_insert": "Canvas",
"cmd.canvas_to_sketch": "Canvas to Sketch",
"cmd.component_activate": "Activate Component",
"cmd.component_break_link": "Break Link",
"cmd.component_capture_position": "Capture Position",
"cmd.component_copies": "Find Identical Copies",
"cmd.component_copy_bodies": "Paste Bodies",
"cmd.component_create": "New Component",
"cmd.component_cycle_color": "Cycle Component Color",
"cmd.component_delete": "Delete Component",
@@ -762,6 +781,7 @@
"cmd.document_revision": "Design Revision",
"cmd.document_units": "Change Design Units",
"cmd.edit_checkpoint": "Checkpoint",
"cmd.edit_repeat": "Repeat Last Command",
"cmd.edit_restore_checkpoint": "Restore Checkpoint",
"cmd.engine_commands": "List Commands",
"cmd.engine_describe": "Describe Command",
@@ -774,7 +794,9 @@
"cmd.file_export": "Export",
"cmd.file_insert_mesh": "Insert Mesh",
"cmd.file_insert_step": "Insert STEP",
"cmd.file_open_in_easyprint": "Open with EasyPrint",
"cmd.file_save_mesh": "Save As Mesh",
"cmd.file_send_to_slicer": "Send to Slicer",
"cmd.file_slicers": "Installed Slicers",
"cmd.frame_member": "Frame Member",
"cmd.frame_profile_insert": "Insert Profile",
@@ -846,6 +868,7 @@
"cmd.occurrence_ground": "Ground",
"cmd.occurrence_move": "Move Occurrence",
"cmd.occurrence_properties": "Occurrence Physical Properties",
"cmd.occurrence_reparent": "Move to Component",
"cmd.parameters_add": "Add User Parameter",
"cmd.parameters_change": "Change Parameters",
"cmd.parameters_comment": "Comment Parameter",
@@ -872,9 +895,11 @@
"cmd.plastic_snap_fit": "Snap Fit",
"cmd.render_aging_describe": "Layered Surface Aging",
"cmd.render_backends": "Render Backends",
"cmd.render_batch": "Render All Shots",
"cmd.render_camera_fit": "Frame Render Subject",
"cmd.render_camera_get": "Inspect Render Camera",
"cmd.render_cancel": "Cancel Render",
"cmd.render_compare": "Compare Images",
"cmd.render_effects_describe": "Lens and Creative Effects",
"cmd.render_export": "Export Render Image",
"cmd.render_hdr_inspect": "Inspect HDR Source",
@@ -890,6 +915,7 @@
"cmd.render_library_surface_prepare": "Prepare Authored Surface Maps",
"cmd.render_library_surface_unbind": "Remove Surface Binding",
"cmd.render_library_targets": "Find Render Library Targets",
"cmd.render_lights_solo": "Solo Light",
"cmd.render_optics_bind": "Save Optical Binding",
"cmd.render_optics_unbind": "Remove Optical Binding",
"cmd.render_preflight": "Check Render Request",
@@ -954,6 +980,7 @@
"cmd.sketch_drag": "Drag Sketch Geometry",
"cmd.sketch_drag_end": "End Sketch Drag",
"cmd.sketch_draw_dimensioned": "Draw Dimensioned Sketch Geometry",
"cmd.sketch_drawing_bounds": "Drawing Size",
"cmd.sketch_edit": "Edit Sketch",
"cmd.sketch_edit_text": "Edit Text",
"cmd.sketch_export_dxf": "Save As DXF",
@@ -998,8 +1025,11 @@
"cmd.sketch_spline_control_point": "Control Point Spline",
"cmd.sketch_spline_control_point_5": "Control Point Spline (Degree 5)",
"cmd.sketch_spline_fit_point": "Fit Point Spline",
"cmd.sketch_spline_insert_point": "Add Spline Point",
"cmd.sketch_spun_profile": "Spun Profile",
"cmd.sketch_toggle_diameter": "Toggle Radius / Diameter",
"cmd.sketch_toggle_driven": "Toggle Driven Dimension",
"cmd.sketch_transform": "Transform",
"cmd.sketch_trim_path": "Trim Along Path",
"cmd.solid_align": "Align",
"cmd.solid_boundary_fill": "Boundary Fill",
@@ -1063,6 +1093,7 @@
"cmd.view_rename": "Rename Named View",
"cmd.view_right": "Right View",
"cmd.view_save": "New Named View",
"cmd.view_shots_strip": "Shots Strip",
"cmd.view_spin": "Continuous orbit",
"cmd.view_tool": "Navigation tool",
"cmd.view_top": "Top View",
@@ -1136,6 +1167,7 @@
"document.save_changes": "Save changes to {name}?",
"document.save_copy": "Save a Copy…",
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
"drive.rigid": "{joint} is a rigid joint: it holds its parts still, so it has nothing to drive. To let them move, change its type in Edit Joint: Revolute to rotate, Slider to slide, Cylindrical for both.",
"drive.stopped_by_contact": "Stopped at {value}: {a} would pass through {b}",
"drive.stopped_by_loop": "Stopped at {value}: the loop through {joint} can't close any further",
"drive.unconnected": "Moves nothing: {part} is not connected to anything grounded",
@@ -1171,28 +1203,44 @@
"from the profile": "from the profile",
"from the rule": "from the rule",
"help.apache": "Apache licence 2.0",
"help.appearance.assign": "Gives the selected bodies, faces or components a colour or a material look, without changing their weight. Use it to tell parts apart or to preview a finish.",
"help.attributions": "Attributions (ATTRIBUTION.md)",
"help.build": "Version {version} · commit {commit} · built {date}",
"help.connect_note": "Restart each assistant to load the SolidCraft server. Each file was backed up as .bak before it changed.",
"help.connect_title": "Connect AI assistants",
"help.face.delete": "Removes the selected faces and lets the neighbouring faces close the gap, or leaves the gap open as a surface. Use it to take off a rounded edge, a hole or a boss that is in the way.",
"help.font_details": "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans (SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.",
"help.fonts": "Fonts",
"help.licences": "Licences",
"help.material.assign": "Sets what the selected bodies are made of, such as steel, aluminium or ABS. Use it so that the mass and weight match the real part.",
"help.mit": "MIT licence",
"help.notice": "NOTICE",
"help.parameters.change": "Opens the table of named values and expressions that drive the dimensions of the design. Use it to change a size in one place and let every feature that uses it follow.",
"help.solid.align": "Moves the selected bodies so that a point, face or plane on them meets a point, face or plane elsewhere. Use it to place a part against another one without measuring the offset.",
"help.solid.chamfer": "Cuts the selected edges with a flat bevel of the distance you set. Use it to break sharp edges or to ease the entry of a pin or screw.",
"help.solid.combine": "Joins bodies, cuts them out of a target body, or keeps only the volume they share, and can keep the tool bodies. Use it to merge parts, to cut one shape with another or to find where two bodies overlap.",
"help.solid.draft": "Tilts the selected faces by the angle you set, measured from a neutral plane. Use it on moulded or cast parts so that they slide out of the mould.",
"help.solid.extrude": "Pushes a sketch profile into 3D along a distance, adding or removing material. Use it to turn a flat outline into a block, a boss or a pocket.",
"help.solid.fillet": "Rounds the selected edges with a smooth circular blend of the radius you set. Use it to soften sharp edges for strength, comfort or looks.",
"help.solid.hole": "Drills a round hole of the diameter you set at a chosen point on a face. Use it for screw clearances, pins and other simple round openings.",
"help.solid.loft": "Blends a smooth solid between two or more sketch profiles, or from a profile to a point. Use it for transitions, funnels, handles and any shape that changes section along its length.",
"help.solid.mirror": "Makes a mirrored copy of the selected bodies or features across a plane, and can merge it with the original. Use it for symmetric parts, so that you model one half and get both.",
"help.solid.move": "Moves or rotates the selected bodies, or a copy of them, by the distance and angle you set. Use it to reposition a body or to make a second one in another place.",
"help.solid.offset_face": "Pushes the selected faces in or out by the distance you set, and the faces around them follow. Use it to make a wall thicker, a pocket deeper or a boss taller without editing the sketch.",
"help.solid.pattern.circular": "Repeats the selected objects around an axis, for the count and total angle you set. Use it for bolt circles, spokes, fins and teeth.",
"help.solid.pattern.rectangular": "Repeats the selected objects in one or two directions with the count and spacing you set. Use it for rows and grids of holes, ribs, pins or feet.",
"help.solid.press_pull": "Adds or removes material as you drag a face, or rounds edges as you drag them. Use it for quick changes straight on the model.",
"help.solid.remove": "Takes the selected bodies out of the design from this point in the timeline on; earlier steps keep them. Use it to drop a helper body, such as a cutting tool you no longer need.",
"help.solid.replace_face": "Moves the selected flat faces onto a plane or onto a parallel face, and the body grows or shrinks to meet it. Use it to bring a face flush with another part or a construction plane.",
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
"help.solid.scale": "Makes the selected bodies larger or smaller by a factor, the same in every direction or one per axis. Use it to resize an imported shape or to allow for shrinkage.",
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
"help.solid.split_body": "Cuts a body into separate bodies with a plane, the face of another body or a sketch line. Use it to make parts that are printed, machined or coloured on their own.",
"help.solid.split_face": "Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
"help.solid.sweep": "Drags a sketch profile along a path to make a solid. Use it for pipes, rails, handles and any part of constant section that follows a curve.",
"help.third_party": "Third-party licences",
"help.timeline.compute_all": "Rebuilds every feature in the timeline from the start. Use it to refresh the model and to see which features fail.",
"help.timeline.delete": "Deletes the selected features from the timeline; deleting a sketch also deletes the features built from it. Use it to remove a step that is no longer wanted.",
"hole_presets.confirm_delete": "Confirm delete",
"hole_presets.delete": "Delete",
"hole_presets.delete_prompt": "Delete this personal preset? Existing holes are unchanged.",
@@ -1243,7 +1291,7 @@
"home.new_mm": "New design",
"home.new_mm_hint": "Millimetres · opens in Design",
"home.open": "Open…",
"home.open_hint": "A .solidcraft design ({shortcut})",
"home.open_hint": "A .sc design ({shortcut})",
"home.open_now": "Open now",
"home.opened_days_ago": "{count} days ago",
"home.opened_hours_ago": "{count} h ago",
@@ -1277,6 +1325,9 @@
"install.steps_intro": "Safari installs web apps from its own menus:",
"install.title": "Install SolidCraft",
"issue.body_label": "Report text",
"issue.note_changed_while_waiting": "Settings or model changed while waiting for the preview. Retake the screenshot to update the technical details for the current view.",
"issue.note_details_kept": "Screenshot captured locally. Your edited technical details were kept: check that they still match the current view.",
"issue.note_details_updated": "Technical details updated to match the current view. Screenshot captured locally. Review it before attaching it on GitHub.",
"issue.note_image_copied": "Paste the image into the GitHub draft, or save the PNG if image copying is unavailable.",
"issue.note_opened": "Review the GitHub draft and attach the screenshot. GitHub's Submit new issue button publishes it.",
"issue.note_opened_copied": "Report copied. Paste into the GitHub draft, attach the screenshot, then click Submit new issue on GitHub.",
@@ -1343,6 +1394,8 @@
"material.editor.unknown_card": "Engineering properties are unknown. Save may keep only the assigned density and existing appearance.",
"material.editor.user_supplied": "User supplied",
"material.editor.wood": "Wood",
"material.selection.choose": "Choose a material",
"material.selection.mixed": "Different materials",
"measure.at": "At",
"measure.item": "Item {number}",
"messages.ago_h": "{n} h ago",
@@ -1371,6 +1424,9 @@
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
"nav.free_orbit.label": "Free orbit",
"nav.free_orbit.snap_help": "Mouse and 3D mouse rotate through both poles and preserve roll. Hold Shift during an orbit drag to snap the view direction and roll.",
"nav.free_orbit_upright.help": "Sideways mouse drags spin the model around the world's vertical axis; up and down drags tilt over the top without a limit.",
"nav.free_orbit_upright.label": "Turn around the vertical axis",
"nav.fusion.help": "Middle drag pans, Shift+middle orbits, right drag orbits",
"nav.inventor.help": "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
"nav.openscad.help": "Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
@@ -1422,6 +1478,10 @@
"prefs.assistant": "Assistant",
"prefs.axes": "Axes",
"prefs.axis": "Axis",
"prefs.bodies_folders": "Bodies folders",
"prefs.bodies_folders_always": "Always",
"prefs.bodies_folders_never": "Never",
"prefs.bodies_folders_several": "Several bodies only",
"prefs.bottom": "Bottom",
"prefs.browser": "Browser",
"prefs.built_in": "Built in",
@@ -1445,6 +1505,11 @@
"prefs.design": "Design defaults",
"prefs.design_note": "What new designs start with. Open designs keep their own.",
"prefs.devices": "3D mouse",
"prefs.dialog_buttons": "Dialog buttons",
"prefs.dialogs": "Dialogs",
"prefs.dialogs_edge": "At the view's edge",
"prefs.dialogs_left_column": "In the left column",
"prefs.dialogs_note": "Where command dialogs open: docked at the view's edge, or in the left column in place of the browser and the History, which come back as they were when the dialog closes.",
"prefs.display": "Display",
"prefs.edges": "Edges and shading",
"prefs.extend": "Extend",
@@ -1467,6 +1532,12 @@
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note": "Presets set the choices below together.",
"prefs.history": "History",
"prefs.history_origin_bodies": "Show origin and bodies at the top",
"prefs.history_origin_bodies_note": "The History on the left or under the browser starts with the active component's Bodies folder and the Origin, with their eyes and menus as in the browser. The History at the bottom is unchanged.",
"prefs.history_sketches": "Sketches",
"prefs.history_sketches_in_order": "In order",
"prefs.history_sketches_note": "With Sketches set to Under their feature, the History at the left lists each sketch one level in, under the first feature built from it, whenever it is wide enough for names. The timeline at the bottom always keeps the creation order.",
"prefs.history_sketches_under_feature": "Under their feature",
"prefs.hold_stop_snapping": "Hold to stop snapping",
"prefs.icons_only": "Icons only",
"prefs.imported_file": "Imported file",
@@ -1498,6 +1569,8 @@
"prefs.opaque": "Opaque",
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
"prefs.orientation": "Orientation",
"prefs.origin_plane_names": "Origin plane names",
"prefs.origin_plane_names_note": "Names shown in the browser, dialogs and lists. {top} is XY, {front} is XZ and {right} is YZ, with Z up. Commands, scripts and saved designs keep XY, XZ and YZ.",
"prefs.overall_speed": "Overall speed",
"prefs.placement": "Placement",
"prefs.prefer_free_sketch_points": "Prefer movable sketch points when points overlap",
@@ -1511,6 +1584,8 @@
"prefs.saving": "Autosave and recovery",
"prefs.scale_note": "Also Ctrl + / Ctrl − in the window. Buttons are 28 px at 100%.",
"prefs.scale_text": "Scale and text",
"prefs.search": "Search settings",
"prefs.search_empty": "No matching settings",
"prefs.select": "Select",
"prefs.select_one_item": "Select one item",
"prefs.selecting": "Selecting",
@@ -1538,11 +1613,18 @@
"prefs.supersampling_brief": "Draws the view larger and filters it down: very crisp, and heavier.",
"prefs.tab_accent_note": "The accent follows the active tab",
"prefs.theme_file": "theme file",
"prefs.theme_follow": "Follow a theme file",
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
"prefs.theme_follow_off": "Off",
"prefs.theme_follow_stop": "Stop following",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "now",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Add the item, or take it out again",
"prefs.toolbar": "Toolbar",
"prefs.top": "Top",
"prefs.translucent": "Translucent",
"prefs.under_browser": "Under browser",
"prefs.under_cursor": "Under cursor",
@@ -1601,6 +1683,7 @@
"render.aging_target_search": "Filter body names",
"render.aging_upward": "Upward-facing deposition",
"render.all_controls_help": "Dense inspection of all current controls. Section pages provide focused editing without changing the underlying request.",
"render.all_shots_label": "All Shots",
"render.appearance_physics_separate": "Visual appearance does not assign density, thermal data or other engineering properties.",
"render.availability_native": "Native composition; review before applying",
"render.availability_projection": "Supported projection; see scope and omissions",
@@ -1755,10 +1838,13 @@
"render.effects_reset_lens": "Reset lens effects",
"render.effects_seed": "Pattern seed",
"render.export_display": "Export: display PNG",
"render.export_image_label": "Export Image",
"render.export_linear": "Export: raw linear EXR",
"render.exr_pending": "GPU can retain raw linear EXR. Compression, multilayer images and auxiliary render passes remain unavailable.",
"render.film_grain": "Photographic grain",
"render.fit": "Fit Product",
"render.fixed_camera": "Camera",
"render.fixed_world": "World",
"render.flare_ghosts": "Ghost rings per source",
"render.flare_halo": "Flare halo",
"render.flare_streak": "Anamorphic-style streak",
@@ -1785,6 +1871,12 @@
"render.hdr_primaries_srgb": "Color primaries: sRGB/D65",
"render.hdr_primaries_undeclared": "The source does not declare primaries; review the renderer color requirements.",
"render.hdr_resource_missing": "This design is missing the saved HDR resource.",
"render.heading_batch": "BATCH",
"render.heading_frame": "FRAME",
"render.heading_lens": "LENS",
"render.heading_results": "RESULTS",
"render.heading_shots": "SHOTS",
"render.hide_shots_label": "Hide Shots strip",
"render.image_advanced": "Advanced GPU controls",
"render.image_cancelled": "Render cancelled",
"render.image_cancelling": "Cancelling",
@@ -1816,6 +1908,7 @@
"render.image_width": "Width",
"render.include_rig_cards": "Include passive cards and flags",
"render.inspect_scene": "Refresh captured scene summary",
"render.inspector": "Inspector",
"render.keep_editing": "Keep editing",
"render.layer_down": "Move down",
"render.layer_enabled": "Layer enabled",
@@ -1884,6 +1977,8 @@
"render.library_unfavorite": "Remove favorite",
"render.library_uniform_ack": "Use the uniform approximation without spatial or advanced effects",
"render.library_usable": "Executable projections",
"render.light_anchor_pending": "Changing the light coordinate frame is not supported yet.",
"render.lights_fixed_to": "Lights fixed to",
"render.linear_help": "Keeps a 32-bit float RGBA master before denoising, exposure, tone mapping, filters and backdrop. Linear sRGB/D65; coverage-premultiplied. Additional memory is admitted explicitly. No compression or extra passes.",
"render.linear_retain": "Retain raw linear EXR",
"render.linear_unavailable": "The selected completed frame has no retained EXR. Enable retention before rendering; an older PNG cannot recover raw radiance.",
@@ -1969,6 +2064,7 @@
"render.output_over_budget": "The requested image exceeds the selected resource envelope. Choose a compatible size or profile explicitly; no automatic downgrade occurs.",
"render.output_ratio_refused": "The linked dimension exceeds this method’s size limit. The previous size is retained; unlock the ratio or choose a smaller value.",
"render.output_swap_orientation": "Swap width and height",
"render.output_tools_inspector": "Edit size and format in the Output inspector.",
"render.page_aging": "Aging & contamination",
"render.page_all": "All controls",
"render.page_appearance": "Appearance",
@@ -1980,6 +2076,7 @@
"render.page_lighting": "Lighting",
"render.page_output": "Output",
"render.page_scene": "Scene",
"render.paint_label": "Paint",
"render.passive_stage": "Ground and passive stage",
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
"render.perspective": "Perspective camera",
@@ -1995,6 +2092,10 @@
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
"render.pick_component": "Component",
"render.pick_label": "Pick",
"render.pick_up_label": "Pick Up",
"render.pick_waiting": "Product picking is not supported yet.",
"render.post_contrast": "Contrast",
"render.post_gain_b": "Blue gain",
"render.post_gain_g": "Green gain",
@@ -2007,6 +2108,7 @@
"render.preview": "Integrated preview",
"render.preview_help": "Raster shading and blended transparency. Refraction, HDRI and final rendering are not available yet.",
"render.preview_image_settings_help": "Live Preview keeps fixed studio lighting. Exposure and image lighting apply to the image job when you Check or Start.",
"render.preview_lighting_help": "The live preview shows direct light only and no shadows.",
"render.preview_retry": "Retry live preview",
"render.preview_unavailable_help": "The live preview is unavailable. Saved setups and image-job controls remain accessible. Change the scene or explicitly retry; a refused scene is not rebuilt on every frame.",
"render.preview_upload": "Live-preview vertex payload",
@@ -2020,8 +2122,10 @@
"render.radial_distortion": "Radial distortion",
"render.regrade_action": "Regrade raw master (no denoising)",
"render.regrade_help": "Creates a separate PNG from the selected completed raw master, using these presentation controls. Does not trace rays, change the captured size/camera/lights, or overwrite the EXR. Denoising is not reapplied: raw masters do not retain its guides. The derivative remains exportable through the same job controls.",
"render.regrade_label": "Regrade",
"render.reload_settings": "Reload Saved Settings",
"render.replace_ground": "Confirm replacement of existing stage",
"render.replace_label": "Replace",
"render.replace_saved_surface": "Replace an existing saved binding for this target",
"render.restore_camera": "Restore Saved Camera",
"render.result_actual_help": "One image pixel per display pixel.",
@@ -2067,6 +2171,7 @@
"render.sampling_title": "Adaptive and transmission sampling",
"render.save_continue": "Save and continue",
"render.save_setup": "Save Setup",
"render.save_shot": "Save Shot",
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
"render.saved_surface_remove": "Remove saved binding",
"render.saved_surfaces": "Saved surface bindings",
@@ -2086,6 +2191,9 @@
"render.setup_runtime_unsupported": "This full job cannot be saved yet: it contains maps or scene-body bindings. Remove those temporary inputs before saving a portable setup.",
"render.setup_save_missing": "The saved render configuration could not be found after the command completed.",
"render.sharpening": "Detail sharpening",
"render.shell_tab_appearance": "RENDER APPEARANCE",
"render.shell_tab_camera": "RENDER CAMERA",
"render.shell_tab_scene": "RENDER SCENE",
"render.shot_duplicate": "Duplicate saved shot",
"render.shot_manager": "Saved shots",
"render.shot_new_name": "New shot name",
@@ -2094,11 +2202,19 @@
"render.shot_remove_title": "Remove saved shot?",
"render.shot_rename": "Rename saved shot",
"render.shot_saved_only": "Save the current draft first. Duplicate and rename preserve exact saved options and resource references.",
"render.show_shots_label": "Show Shots strip",
"render.sky_ground": "Ground · linear RGB",
"render.sky_horizon": "Horizon · linear RGB",
"render.sky_zenith": "Zenith · linear RGB",
"render.soft_focus": "Soft focus",
"render.stage_frame": "Frame",
"render.stage_light": "Light",
"render.stage_look": "Look",
"render.stage_orthographic": "Orthographic",
"render.stage_output": "Output",
"render.stage_panel_count": "Saved panels",
"render.stage_preview_note": "Integrated preview · direct light",
"render.strip_label": "Strip",
"render.studio_add_area": "Add area light",
"render.studio_ambient": "Constant environment radiance",
"render.studio_ambient_rgb": "Constant environment radiance (linear RGB)",
@@ -2155,6 +2271,12 @@
"render.volume_max": "Maximum bounds · world mm",
"render.volume_min": "Minimum bounds · world mm",
"render.volume_title": "Bounded homogeneous volume",
"render.wait_batch": "not supported yet: the saved-shot render queue",
"render.wait_compare": "not supported yet: retained image comparison",
"render.wait_paint": "not supported yet: product picking and appearance painting",
"render.wait_pick_up": "not supported yet: product picking and appearance sampling",
"render.wait_replace": "not supported yet: replacing all uses of an appearance",
"render.wait_solo": "not supported yet: light visibility and solo controls",
"sample.bracket.about": "Base flange with edge flanges and a hem on a 2 mm rule",
"sample.bracket.name": "Sheet Metal Bracket",
"sample.enclosure.about": "Shelled box with a lip, screw bosses, a rest and a snap fit",
@@ -2246,6 +2368,7 @@
"timeline.delete_after_marker_one": "Delete 1 Feature After History Marker",
"timeline.delete_after_marker_other": "Delete {count} Features After History Marker",
"timeline.delete_folders_tip": "Keep their features and nested folders in the history",
"timeline.delete_folders_with_features_tip": "Delete the folders with every feature in them and their nested folders",
"timeline.delete_many": "Delete {count} Selected Features",
"timeline.feature_error": "Error: {message}",
"timeline.feature_hint": "Double-click to edit · drag to reorder · right-click for more",
@@ -2255,6 +2378,7 @@
"timeline.move_to_group": "Move Features to Folder",
"timeline.rename_group": "Rename Feature Folder",
"timeline.reorder_many": "Reorder Features",
"timeline.suppress_folders_tip": "Suppress or unsuppress every feature in the folders and their nested folders",
"timeline.ungroup": "Remove Feature Folders",
"to the face": "to the face",
"to the next face": "to the next face",
@@ -2265,7 +2389,11 @@
"toolbar.render_camera": "CAMERA",
"toolbar.render_environment": "ENVIRONMENT",
"toolbar.render_lighting": "LIGHTING",
"toolbar.render_lights": "LIGHTS",
"toolbar.render_result": "RESULT",
"toolbar.render_scene": "SCENE",
"toolbar.render_stage": "STAGE",
"toolbar.render_studio": "STUDIO",
"trace.points": "{count} points along the path",
"trace.stopped_by_contact": "Ends at sample {sample}: {a} would pass through {b}",
"trace.stopped_by_loop": "Ends at sample {sample}: the loop through {joint} can't close any further",
@@ -2444,6 +2572,7 @@
"ui.agent_chat_import_failed": "{name}: the import failed: {error}",
"ui.agent_chat_intro": "Chat with a coding agent you already use, or a model endpoint you choose. It works on the open design through SolidCraft's tools: its changes show as it makes them and undo as one step per message. What it may do without asking is set in Preferences > AI.",
"ui.agent_chat_key_in_env": "key set",
"ui.agent_chat_last_missing": "The agent used last ({agent}) is not available any more. Pick an agent, then press Retry.",
"ui.agent_chat_left": "Left",
"ui.agent_chat_limit": "The agent reached the limit of {limit} changes for one message. Send another message to let it go on.",
"ui.agent_chat_login_found": "login found",
@@ -2477,7 +2606,7 @@
"ui.agent_chat_pop_out": "Open in its own window, which you can move to another screen",
"ui.agent_chat_previous_picture": "Previous picture",
"ui.agent_chat_privacy": "Nothing goes anywhere except to the agent you run, under your own account.",
"ui.agent_chat_privacy_note": "SolidCraft sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_privacy_note": "The chat itself sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_provider_models": "{count} models",
"ui.agent_chat_provider_other": "Other",
"ui.agent_chat_provider_tip": "The provider whose models the next list shows.",
@@ -2490,6 +2619,7 @@
"ui.agent_chat_remove": "Remove from the queue",
"ui.agent_chat_remove_file": "Take it off this message",
"ui.agent_chat_remove_imported": "Take it off this message; the part stays in the design (Undo removes it)",
"ui.agent_chat_retry": "Retry",
"ui.agent_chat_right": "Right",
"ui.agent_chat_save": "Save",
"ui.agent_chat_send": "Send",
@@ -2502,6 +2632,7 @@
"ui.agent_chat_state_auth": "Sign-in needed",
"ui.agent_chat_state_busy": "Working…",
"ui.agent_chat_state_exited": "Agent stopped",
"ui.agent_chat_state_failed": "Connection failed",
"ui.agent_chat_state_off": "Not connected",
"ui.agent_chat_state_ready": "Ready",
"ui.agent_chat_state_starting": "Starting…",
@@ -2695,6 +2826,7 @@
"ui.command_done": "{command} done",
"ui.command_palette_hint": "Type a command, e.g. {example} — or press S to search",
"ui.concentric": "Concentric",
"ui.cone": "Cone",
"ui.conic_curve": "Conic Curve",
"ui.constant": "Constant",
"ui.constraints_conflict": "Constraints conflict",
@@ -2755,6 +2887,11 @@
"ui.draft": "Draft",
"ui.drawing": "Drawing",
"ui.drill_point": "Drill Point",
"ui.easyprint_consent": "EasyPrint is Printables' slicer in the browser. To open the model there, SolidCraft uploads it to Printables' servers. The link expires after 30 minutes, and you'll need to be signed in to Printables. Continue?",
"ui.easyprint_failed": "EasyPrint upload failed: {error}",
"ui.easyprint_opened": "Opened in EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, in the browser)",
"ui.easyprint_uploading": "Uploading to Printables…",
"ui.edit": "Edit",
"ui.editJointLimits": "Edit Joint Limits",
"ui.edit_section_analysis": "Edit Section Analysis",
@@ -2799,6 +2936,7 @@
"ui.fully_constrained": "Fully constrained",
"ui.function": "Function",
"ui.general": "General",
"ui.gpu_memory_lowered": "The graphics card is out of memory: the view is drawn at a lower quality.",
"ui.graphics": "Graphics",
"ui.graphics_custom": "Custom",
"ui.graphics_performance": "Performance",
@@ -2817,6 +2955,7 @@
"ui.heading_circular_pattern": "CIRCULAR PATTERN",
"ui.heading_coil": "COIL",
"ui.heading_combine": "COMBINE",
"ui.heading_cone": "CONE",
"ui.heading_configure": "CONFIGURE",
"ui.heading_constraints": "CONSTRAINTS",
"ui.heading_construct": "CONSTRUCT",
@@ -2897,6 +3036,7 @@
"ui.history_names": "Names",
"ui.history_play": "Play to the end",
"ui.history_show_names": "Show names",
"ui.history_sketch_listed_under": "Listed under {feature}",
"ui.history_title": "History",
"ui.history_toggle_names": "Show or hide feature names",
"ui.hole": "Hole",
@@ -3067,6 +3207,7 @@
"ui.render": "Render",
"ui.renderer": "Renderer",
"ui.repeat": "Repeat",
"ui.repeat_nothing": "Nothing to repeat yet: start a tool or dialog first.",
"ui.replace_with_link_done": "The copies are now placements of the linked design.",
"ui.report_a_bug": "Report a bug",
"ui.reset": "Reset",
@@ -3085,9 +3226,8 @@
"ui.right": "Right",
"ui.save": "Save",
"ui.save_as": "Save As",
"ui.save_mesh_no_slicer": "No slicer found: choose Custom… and pick your slicer program, or turn off Send to slicer",
"ui.save_mesh_no_slicer_open": "No slicer found: choose Custom… and pick your slicer program",
"ui.save_mesh_pick_bodies": "Select the bodies to save",
"ui.save_mesh_temp_failed": "Couldn't make {folder}: {error}",
"ui.saved_to": "Saved {path}",
"ui.sbox_pin": "Pin to the top of the S box",
"ui.sbox_unpin": "Unpin",
@@ -3102,8 +3242,9 @@
"ui.selection_shift_add_only": "Shift+click adds without removing",
"ui.selection_shift_add_only_tip": "On: Shift adds items, Ctrl/Cmd toggles. Off (default): Shift and Ctrl/Cmd both toggle.",
"ui.send": "Send",
"ui.send_to_slicer": "Send to slicer",
"ui.send_to_slicer_unavailable": "Send to Slicer needs the desktop app: a browser can't start a program",
"ui.sent_to_slicer": "Sent to {slicer}",
"ui.sent_to_slicer_copy": "Sent to {slicer} as {file}, because the first file is still open there",
"ui.sheet_gap": "Gap",
"ui.shell": "Shell",
"ui.shortcut_capture_add_key": "Add one ordinary key. Escape cancels.",
@@ -3138,6 +3279,15 @@
"ui.slice": "Slice",
"ui.slicer": "Slicer",
"ui.slicer_delete_tip": "{program}\nPress Delete to remove it from the list",
"ui.slicer_lost": "{slicer} was started but can't be found again to send it the file. The file is saved at {path}: open it from there.",
"ui.slicer_no_file": "{slicer} is ready, but there is no file to send to it.",
"ui.slicer_no_response": "{slicer} didn't respond in time. The file is already saved at {path}: send it again once the slicer has started.",
"ui.slicer_resend_hint": "Sending again replaces the file the slicer has open.",
"ui.slicer_single_instance": "Open files in a slicer that is already running",
"ui.slicer_single_instance_tip": "PrusaSlicer, SuperSlicer and Cura load the file in the window that is open instead of starting another. Other slicers are started as they are. PrusaSlicer 3.0 always does this: its launcher can't take a file any other way.",
"ui.slicer_starting": "Starting {slicer}…",
"ui.slicer_still_starting": "Still waiting for {slicer} to start. Nothing else was sent.",
"ui.slicer_stl_fallback": "The 3MF could not be written, so this is an STL.",
"ui.slot": "Slot",
"ui.smooth": "Smooth",
"ui.smooth_wheel_zoom": "Smooth wheel zoom",
@@ -3205,6 +3355,7 @@
"ui.untitled": "Untitled",
"ui.up_axis_note": "Which axis points up in the views (axis view, Home, Look At). Designs are unchanged.",
"ui.updateToCurrentView": "Update to Current View",
"ui.upload": "Upload",
"ui.vertical_constraint": "Vertical Constraint",
"ui.view_centre": "View centre",
"ui.viewport_theme_follow": "Follow theme",
@@ -3279,5 +3430,6 @@
"web.open_link": "Open linked design?",
"web.open_link_confirm": "There are unsaved changes. Open the linked design in a new tab?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ choose · Enter / Tab complete · Esc dismiss",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ choose · Enter applies what you typed · Esc dismiss"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ choose · Enter applies what you typed · Esc dismiss",
"⌘Enter continues on GitHub.": "⌘Enter continues on GitHub."
}
+19 -5
View File
@@ -463,6 +463,7 @@
"cmd.canvas_delete": "Eliminar lienzo",
"cmd.canvas_edit": "Editar lienzo",
"cmd.canvas_insert": "Lienzo",
"cmd.canvas_to_sketch": "Boceto desde lienzo",
"cmd.component_activate": "Activar componente",
"cmd.component_break_link": "Romper vínculo",
"cmd.component_capture_position": "Capturar posición",
@@ -540,6 +541,7 @@
"cmd.file_export": "Exportar",
"cmd.file_insert_mesh": "Insertar malla",
"cmd.file_insert_step": "Insertar STEP",
"cmd.file_open_in_easyprint": "Abrir con EasyPrint",
"cmd.file_save_mesh": "Guardar como malla",
"cmd.file_slicers": "Laminadores instalados",
"cmd.frame_member": "Miembro estructural",
@@ -757,6 +759,7 @@
"cmd.sketch_spline_control_point_5": "Spline por puntos de control (grado 5)",
"cmd.sketch_spline_fit_point": "Spline por puntos de ajuste",
"cmd.sketch_spun_profile": "Perfil de revolución",
"cmd.sketch_toggle_diameter": "Alternar radio / diámetro",
"cmd.sketch_toggle_driven": "Alternar cota de referencia",
"cmd.sketch_trim_path": "Recortar a lo largo de la trayectoria",
"cmd.solid_align": "Alinear",
@@ -941,7 +944,7 @@
"home.new_mm": "Nuevo diseño",
"home.new_mm_hint": "Milímetros · se abre en Diseño",
"home.open": "Abrir…",
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
"home.open_hint": "Un diseño .sc ({shortcut})",
"home.open_now": "Abrir ahora",
"home.opened_days_ago": "Hace {count} días",
"home.opened_hours_ago": "Hace {count} h",
@@ -1087,6 +1090,12 @@
"pick one or two vertices, edges or faces": "selecciona uno o dos vértices, aristas o caras",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Seguir un archivo de tema",
"prefs.theme_follow_error": "No aplicado: {error}. Se mantiene el último tema válido.",
"prefs.theme_follow_launch": "Solo en esta sesión (--theme-file); se recarga cada vez que otra herramienta reescribe el archivo.",
"prefs.theme_follow_note": "Se recarga cada vez que otra herramienta reescribe el archivo. Elegir un tema deja de seguirlo.",
"prefs.theme_follow_off": "Desactivado",
"prefs.theme_follow_stop": "Dejar de seguir",
"properties.title": "Propiedades: {name}",
"recovery.days_ago": "hace {count} días",
"recovery.detail": "{file} · {features} operaciones · autoguardado {when}",
@@ -1975,7 +1984,7 @@
"ui.agent_chat_pop_out": "Abrir en su propia ventana, que puedes mover a otra pantalla",
"ui.agent_chat_previous_picture": "Imagen anterior",
"ui.agent_chat_privacy": "Solo se envían datos al agente que ejecutes, con tu propia cuenta.",
"ui.agent_chat_privacy_note": "SolidCraft no envía nada a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
"ui.agent_chat_privacy_note": "El chat no envía nada por sí mismo a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
"ui.agent_chat_queue": "Poner en cola",
"ui.agent_chat_queue_full": "Hay demasiados mensajes en espera: elimina uno o espera a que termine el agente.",
"ui.agent_chat_queued": "En cola",
@@ -2162,6 +2171,7 @@
"ui.command_done": "{command} completado",
"ui.command_palette_hint": "Escribe un comando, p. ej., {example}, o pulsa S para buscar",
"ui.concentric": "Concéntrico",
"ui.cone": "Cono",
"ui.conic_curve": "Curva cónica",
"ui.constant": "Constante",
"ui.constraints_conflict": "Conflicto entre restricciones",
@@ -2220,6 +2230,11 @@
"ui.draft": "Ángulo de salida",
"ui.drawing": "Dibujo",
"ui.drill_point": "Punta de broca",
"ui.easyprint_consent": "EasyPrint es el slicer de Printables en el navegador. Para abrir el modelo allí, SolidCraft lo sube a los servidores de Printables. El enlace caduca a los 30 minutos y tendrás que haber iniciado sesión en Printables. ¿Continuar?",
"ui.easyprint_failed": "No se pudo subir a EasyPrint: {error}",
"ui.easyprint_opened": "Abierto en EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, en el navegador)",
"ui.easyprint_uploading": "Subiendo a Printables…",
"ui.edit": "Editar",
"ui.editJointLimits": "Editar límites de unión",
"ui.edit_section_analysis": "Editar análisis de sección",
@@ -2276,6 +2291,7 @@
"ui.heading_circular_pattern": "PATRÓN CIRCULAR",
"ui.heading_coil": "HÉLICE",
"ui.heading_combine": "COMBINAR",
"ui.heading_cone": "CONO",
"ui.heading_configure": "CONFIGURAR",
"ui.heading_constraints": "RESTRICCIONES",
"ui.heading_construct": "CONSTRUIR",
@@ -2512,9 +2528,7 @@
"ui.right": "Derecha",
"ui.save": "Guardar",
"ui.save_as": "Guardar como",
"ui.save_mesh_no_slicer": "No se ha encontrado ningún laminador: elige Personalizado… y selecciona tu laminador, o desactiva Enviar al laminador",
"ui.save_mesh_pick_bodies": "Selecciona los cuerpos que se van a guardar",
"ui.save_mesh_temp_failed": "No se ha podido crear {folder}: {error}",
"ui.saved_to": "Guardado en {path}",
"ui.sbox_pin": "Fijar arriba en la búsqueda S",
"ui.sbox_unpin": "Desfijar",
@@ -2529,7 +2543,6 @@
"ui.selection_shift_add_only": "Mayús+clic añade sin quitar",
"ui.selection_shift_add_only_tip": "Activado: Mayús añade elementos, Ctrl/Cmd alterna. Desactivado (predeterminado): Mayús y Ctrl/Cmd alternan.",
"ui.send": "Enviar",
"ui.send_to_slicer": "Enviar al laminador",
"ui.sent_to_slicer": "Enviado a {slicer}",
"ui.sheet_gap": "Separación",
"ui.shell": "Vaciado",
@@ -2621,6 +2634,7 @@
"ui.untitled": "Sin título",
"ui.up_axis_note": "Eje que apunta hacia arriba en las vistas (vista de eje, Inicio, Mirar hacia). Los diseños no cambian.",
"ui.updateToCurrentView": "Actualizar a vista actual",
"ui.upload": "Subir",
"ui.view_centre": "Centro de la vista",
"ui.visual_shaded_hidden_edges": "Sombreado con aristas ocultas",
"ui.visual_shaded_with_edges": "Sombreado con aristas",
+1192 -9
View File
File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -457,6 +457,7 @@
"cmd.canvas_delete": "Eliminar o lenzo",
"cmd.canvas_edit": "Editar o lenzo",
"cmd.canvas_insert": "Lenzo",
"cmd.canvas_to_sketch": "Bosquexo desde o lenzo",
"cmd.component_activate": "Activar o compoñente",
"cmd.component_break_link": "Romper a ligazón",
"cmd.component_capture_position": "Capturar a posición",
@@ -534,6 +535,7 @@
"cmd.file_export": "Exportar",
"cmd.file_insert_mesh": "Inserir malla",
"cmd.file_insert_step": "Inserir STEP",
"cmd.file_open_in_easyprint": "Abrir con EasyPrint",
"cmd.file_save_mesh": "Gardar como malla",
"cmd.file_slicers": "Laminadores instalados",
"cmd.frame_member": "Membro estrutural",
@@ -898,7 +900,7 @@
"home.new_mm": "Novo deseño",
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
"home.open": "Abrir…",
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
"home.open_hint": "Un deseño .sc ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Hai {count} días",
"home.opened_hours_ago": "Hai {count} h",
@@ -1001,6 +1003,12 @@
"pick one or two vertices, edges or faces": "escolle un ou dous vértices, arestas ou caras",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Seguir un ficheiro de tema",
"prefs.theme_follow_error": "Non aplicado: {error}. Mantense o último tema válido.",
"prefs.theme_follow_launch": "Só nesta sesión (--theme-file); vólvese cargar cando outra ferramenta reescribe o ficheiro.",
"prefs.theme_follow_note": "Vólvese cargar cando outra ferramenta reescribe o ficheiro. Escoller un tema detén o seguimento.",
"prefs.theme_follow_off": "Desactivado",
"prefs.theme_follow_stop": "Deixar de seguir",
"properties.title": "Propiedades: {name}",
"recovery.days_ago": "hai {count} días",
"recovery.detail": "{file} · {features} operacións · gardado automaticamente {when}",
@@ -1326,7 +1334,7 @@
"ui.agent_chat_pop_out": "Abrir nunha xanela propia, que podes mover a outra pantalla",
"ui.agent_chat_previous_picture": "Imaxe anterior",
"ui.agent_chat_privacy": "Nada vai a ningún sitio agás ao axente que executas, coa túa propia conta.",
"ui.agent_chat_privacy_note": "SolidCraft non envía nada a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
"ui.agent_chat_privacy_note": "O chat non envía nada por si mesmo a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
"ui.agent_chat_queue": "Pór na cola",
"ui.agent_chat_queue_full": "Hai demasiadas mensaxes agardando: quita unha ou agarda polo axente.",
"ui.agent_chat_queued": "Na cola",
@@ -1511,6 +1519,7 @@
"ui.command_done": "{command} feito",
"ui.command_palette_hint": "Escribe unha orde, p. ex. {example} — ou preme S para buscar",
"ui.concentric": "Concéntrica",
"ui.cone": "Cono",
"ui.conic_curve": "Curva cónica",
"ui.constant": "Constante",
"ui.constraints_conflict": "Conflito de restricións",
@@ -1569,6 +1578,11 @@
"ui.draft": "Ángulo de desmoldaxe",
"ui.drawing": "Debuxo",
"ui.drill_point": "Punta de broca",
"ui.easyprint_consent": "EasyPrint é o slicer de Printables no navegador. Para abrir o modelo alí, SolidCraft cárgao nos servidores de Printables. A ligazón caduca aos 30 minutos e terás que ter a sesión iniciada en Printables. Continuar?",
"ui.easyprint_failed": "Non se puido cargar a EasyPrint: {error}",
"ui.easyprint_opened": "Aberto en EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, no navegador)",
"ui.easyprint_uploading": "Cargando a Printables…",
"ui.edit": "Editar",
"ui.editJointLimits": "Editar os límites da unión",
"ui.edit_section_analysis": "Editar a análise de sección",
@@ -1625,6 +1639,7 @@
"ui.heading_circular_pattern": "PATRÓN CIRCULAR",
"ui.heading_coil": "HÉLICE",
"ui.heading_combine": "COMBINAR",
"ui.heading_cone": "CONO",
"ui.heading_configure": "CONFIGURAR",
"ui.heading_constraints": "RESTRICIÓNS",
"ui.heading_construct": "CONSTRUÍR",
@@ -1860,9 +1875,7 @@
"ui.right": "Dereita",
"ui.save": "Gardar",
"ui.save_as": "Gardar como",
"ui.save_mesh_no_slicer": "Non se atopou ningún laminador: escolle Personalizado… e selecciona o teu programa laminador, ou desactiva Enviar ao laminador",
"ui.save_mesh_pick_bodies": "Selecciona os corpos que queres gardar",
"ui.save_mesh_temp_failed": "Non se puido crear {folder}: {error}",
"ui.saved_to": "Gardado {path}",
"ui.sbox_pin": "Fixar arriba na caixa S",
"ui.sbox_unpin": "Desfixar",
@@ -1877,7 +1890,6 @@
"ui.selection_shift_add_only": "Maiús+clic engade sen quitar",
"ui.selection_shift_add_only_tip": "Activado: Maiús engade elementos, Ctrl/Cmd alterna. Desactivado (predeterminado): Maiús e Ctrl/Cmd alternan ambos.",
"ui.send": "Enviar",
"ui.send_to_slicer": "Enviar ao laminador",
"ui.sent_to_slicer": "Enviado a {slicer}",
"ui.sheet_gap": "Separación",
"ui.shell": "Baleirado",
@@ -1969,6 +1981,7 @@
"ui.untitled": "Sen título",
"ui.up_axis_note": "Que eixe apunta cara arriba nas vistas (vista de eixe, Inicio, Mirar cara a). Os deseños non cambian.",
"ui.updateToCurrentView": "Actualizar á vista actual",
"ui.upload": "Cargar",
"ui.view_centre": "Centro da vista",
"ui.visual_shaded_hidden_edges": "Sombreado con arestas ocultas",
"ui.visual_shaded_with_edges": "Sombreado con arestas",
File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -456,6 +456,7 @@
"cmd.canvas_delete": "Vászon törlése",
"cmd.canvas_edit": "Vászon szerkesztése",
"cmd.canvas_insert": "Vászon",
"cmd.canvas_to_sketch": "Vázlat vászonból",
"cmd.component_activate": "Komponens aktiválása",
"cmd.component_break_link": "Kapcsolat megszakítása",
"cmd.component_capture_position": "Pozíció rögzítése",
@@ -533,6 +534,7 @@
"cmd.file_export": "Exportálás",
"cmd.file_insert_mesh": "Háló beszúrása",
"cmd.file_insert_step": "STEP beszúrása",
"cmd.file_open_in_easyprint": "Megnyitás EasyPrinttel",
"cmd.file_save_mesh": "Mentés hálóként",
"cmd.file_slicers": "Telepített szeletelők",
"cmd.frame_member": "Szerkezeti elem",
@@ -897,7 +899,7 @@
"home.new_mm": "Új terv",
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
"home.open": "Megnyitás…",
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
"home.open_hint": "Egy .sc terv ({shortcut})",
"home.open_now": "Jelenleg megnyitva",
"home.opened_days_ago": "{count} napja",
"home.opened_hours_ago": "{count} órája",
@@ -1000,6 +1002,12 @@
"pick one or two vertices, edges or faces": "jelöljön ki egy vagy két csúcsot, élt vagy lapot",
"prefs.cpu": "CPU (szoftveres)",
"prefs.minutes_suffix": " perc",
"prefs.theme_follow": "Témafájl követése",
"prefs.theme_follow_error": "Nem alkalmazható: {error}. Az utolsó érvényes téma marad aktív.",
"prefs.theme_follow_launch": "Követés csak erre a futásra (--theme-file); újratöltés, ha más eszköz felülírja a fájlt.",
"prefs.theme_follow_note": "Újratöltés, ha más eszköz felülírja a fájlt. Téma választása leállítja a követést.",
"prefs.theme_follow_off": "Ki",
"prefs.theme_follow_stop": "Követés leállítása",
"properties.title": "Tulajdonságok: {name}",
"recovery.days_ago": "{count} napja",
"recovery.detail": "{file} · {features} elem · automatikusan mentve: {when}",
@@ -1269,7 +1277,7 @@
"ui.agent_chat_pop_out": "Megnyitás saját ablakban, amelyet másik képernyőre mozgathat",
"ui.agent_chat_previous_picture": "Előző kép",
"ui.agent_chat_privacy": "Semmi sem megy sehová, csak az Ön által futtatott ügynökhöz, az Ön saját fiókja alatt.",
"ui.agent_chat_privacy_note": "A SolidCraft semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
"ui.agent_chat_privacy_note": "A csevegés önmagától semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
"ui.agent_chat_queue": "Sorba",
"ui.agent_chat_queue_full": "Túl sok üzenet vár: távolítson el egyet, vagy várja meg az ügynököt.",
"ui.agent_chat_queued": "Sorban",
@@ -1453,6 +1461,7 @@
"ui.command_done": "{command} kész",
"ui.command_palette_hint": "Írjon be egy parancsot, pl. {example} – vagy nyomja meg az S-t a kereséshez",
"ui.concentric": "Koncentrikus",
"ui.cone": "Kúp",
"ui.conic_curve": "Kúpgörbe",
"ui.constant": "Állandó",
"ui.constraints_conflict": "Kényszerek ütköznek",
@@ -1511,6 +1520,11 @@
"ui.draft": "Dőlés",
"ui.drawing": "Rajz",
"ui.drill_point": "Fúrócsúcs",
"ui.easyprint_consent": "Az EasyPrint a Printables szeletelője a böngészőben. A modell ott való megnyitásához a SolidCraft feltölti azt a Printables szervereire. A hivatkozás 30 perc után lejár, és be kell jelentkeznie a Printablesbe. Folytatja?",
"ui.easyprint_failed": "Az EasyPrintre feltöltés sikertelen: {error}",
"ui.easyprint_opened": "Megnyitva az EasyPrintben",
"ui.easyprint_row": "EasyPrint (Printables, a böngészőben)",
"ui.easyprint_uploading": "Feltöltés a Printablesbe…",
"ui.edit": "Szerkesztés",
"ui.editJointLimits": "Csuklókorlátok szerkesztése",
"ui.edit_section_analysis": "Metszetelemzés szerkesztése",
@@ -1567,6 +1581,7 @@
"ui.heading_circular_pattern": "KÖRMINTA",
"ui.heading_coil": "SPIRÁL",
"ui.heading_combine": "EGYESÍTÉS",
"ui.heading_cone": "KÚP",
"ui.heading_configure": "KONFIGURÁLÁS",
"ui.heading_constraints": "KÉNYSZEREK",
"ui.heading_construct": "SZERKESZTÉS",
@@ -1802,9 +1817,7 @@
"ui.right": "Jobb",
"ui.save": "Mentés",
"ui.save_as": "Mentés másként",
"ui.save_mesh_no_slicer": "Nem található szeletelő: válassza az Egyéni… lehetőséget és jelölje ki a szeletelőprogramot, vagy kapcsolja ki a Küldés szeletelőnek beállítást",
"ui.save_mesh_pick_bodies": "Jelölje ki a mentendő testeket",
"ui.save_mesh_temp_failed": "A(z) {folder} nem hozható létre: {error}",
"ui.saved_to": "Mentve: {path}",
"ui.sbox_pin": "Rögzítés az S doboz tetejére",
"ui.sbox_unpin": "Rögzítés feloldása",
@@ -1819,7 +1832,6 @@
"ui.selection_shift_add_only": "Shift+kattintás eltávolítás nélkül hozzáad",
"ui.selection_shift_add_only_tip": "Be: a Shift hozzáad, a Ctrl/Cmd vált. Ki (alapért.): a Shift és a Ctrl/Cmd is vált.",
"ui.send": "Küldés",
"ui.send_to_slicer": "Küldés szeletelőnek",
"ui.sent_to_slicer": "Elküldve ide: {slicer}",
"ui.sheet_gap": "Rés",
"ui.shell": "Héjazás",
@@ -1911,6 +1923,7 @@
"ui.untitled": "Névtelen",
"ui.up_axis_note": "Melyik tengely mutat felfelé a nézetekben (tengelynézet, Kezdőlap, Ránézés). A tervek változatlanok.",
"ui.updateToCurrentView": "Frissítés az aktuális nézetre",
"ui.upload": "Feltöltés",
"ui.view_centre": "Nézet középpontja",
"ui.visual_shaded_hidden_edges": "Árnyalt rejtett élekkel",
"ui.visual_shaded_with_edges": "Árnyalt élekkel",
+18 -5
View File
@@ -456,6 +456,7 @@
"cmd.canvas_delete": "Hapus Kanvas",
"cmd.canvas_edit": "Edit Kanvas",
"cmd.canvas_insert": "Kanvas",
"cmd.canvas_to_sketch": "Kanvas ke Sketsa",
"cmd.component_activate": "Aktifkan Komponen",
"cmd.component_break_link": "Putuskan Tautan",
"cmd.component_capture_position": "Tangkap Posisi",
@@ -533,6 +534,7 @@
"cmd.file_export": "Ekspor",
"cmd.file_insert_mesh": "Sisipkan Mesh",
"cmd.file_insert_step": "Sisipkan STEP",
"cmd.file_open_in_easyprint": "Buka dengan EasyPrint",
"cmd.file_save_mesh": "Simpan Sebagai Mesh",
"cmd.file_slicers": "Slicer Terpasang",
"cmd.frame_member": "Batang Rangka",
@@ -897,7 +899,7 @@
"home.new_mm": "Desain baru",
"home.new_mm_hint": "Milimeter · terbuka di Desain",
"home.open": "Buka…",
"home.open_hint": "Desain .solidcraft ({shortcut})",
"home.open_hint": "Desain .sc ({shortcut})",
"home.open_now": "Sedang terbuka",
"home.opened_days_ago": "{count} hari lalu",
"home.opened_hours_ago": "{count} jam lalu",
@@ -1000,6 +1002,12 @@
"pick one or two vertices, edges or faces": "pilih satu atau dua titik sudut, tepi, atau muka",
"prefs.cpu": "CPU (perangkat lunak)",
"prefs.minutes_suffix": " mnt",
"prefs.theme_follow": "Ikuti file tema",
"prefs.theme_follow_error": "Tidak diterapkan: {error}. Tema valid terakhir tetap aktif.",
"prefs.theme_follow_launch": "Diikuti hanya untuk sesi ini (--theme-file); dimuat ulang setiap kali alat lain menulis ulang file.",
"prefs.theme_follow_note": "Dimuat ulang setiap kali alat lain menulis ulang file. Memilih tema menghentikan pengikutan.",
"prefs.theme_follow_off": "Nonaktif",
"prefs.theme_follow_stop": "Berhenti mengikuti",
"properties.title": "Properti: {name}",
"recovery.days_ago": "{count} hari lalu",
"recovery.detail": "{file} · {features} fitur · disimpan otomatis {when}",
@@ -1269,7 +1277,7 @@
"ui.agent_chat_pop_out": "Buka di jendela sendiri, yang dapat dipindah ke layar lain",
"ui.agent_chat_previous_picture": "Gambar sebelumnya",
"ui.agent_chat_privacy": "Tidak ada yang dikirim ke mana pun kecuali ke agen yang Anda jalankan, di bawah akun Anda sendiri.",
"ui.agent_chat_privacy_note": "SolidCraft tidak mengirim apa pun ke mana pun: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
"ui.agent_chat_privacy_note": "Obrolan tidak mengirim apa pun ke mana pun dengan sendirinya: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
"ui.agent_chat_queue": "Antre",
"ui.agent_chat_queue_full": "Terlalu banyak pesan menunggu: hapus satu atau tunggu agen.",
"ui.agent_chat_queued": "Mengantre",
@@ -1453,6 +1461,7 @@
"ui.command_done": "{command} selesai",
"ui.command_palette_hint": "Ketik perintah, mis. {example} — atau tekan S untuk mencari",
"ui.concentric": "Konsentris",
"ui.cone": "Kerucut",
"ui.conic_curve": "Kurva Konik",
"ui.constant": "Konstan",
"ui.constraints_conflict": "Batasan bertentangan",
@@ -1511,6 +1520,11 @@
"ui.draft": "Draft",
"ui.drawing": "Gambar Kerja",
"ui.drill_point": "Ujung Bor",
"ui.easyprint_consent": "EasyPrint adalah slicer Printables di browser. Untuk membuka model di sana, SolidCraft mengunggahnya ke server Printables. Tautan kedaluwarsa setelah 30 menit, dan Anda harus masuk ke Printables. Lanjutkan?",
"ui.easyprint_failed": "Gagal mengunggah ke EasyPrint: {error}",
"ui.easyprint_opened": "Dibuka di EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, di browser)",
"ui.easyprint_uploading": "Mengunggah ke Printables…",
"ui.edit": "Edit",
"ui.editJointLimits": "Edit Batas Sambungan",
"ui.edit_section_analysis": "Edit Analisis Penampang",
@@ -1567,6 +1581,7 @@
"ui.heading_circular_pattern": "POLA MELINGKAR",
"ui.heading_coil": "KOIL",
"ui.heading_combine": "GABUNGKAN",
"ui.heading_cone": "KERUCUT",
"ui.heading_configure": "KONFIGURASI",
"ui.heading_constraints": "BATASAN",
"ui.heading_construct": "KONSTRUKSI",
@@ -1802,9 +1817,7 @@
"ui.right": "Kanan",
"ui.save": "Simpan",
"ui.save_as": "Simpan Sebagai",
"ui.save_mesh_no_slicer": "Slicer tidak ditemukan: pilih Kustom… dan tunjuk program slicer Anda, atau matikan Kirim ke slicer",
"ui.save_mesh_pick_bodies": "Pilih bodi yang akan disimpan",
"ui.save_mesh_temp_failed": "Tidak dapat membuat {folder}: {error}",
"ui.saved_to": "Disimpan {path}",
"ui.sbox_pin": "Sematkan di atas kotak S",
"ui.sbox_unpin": "Lepas sematan",
@@ -1819,7 +1832,6 @@
"ui.selection_shift_add_only": "Shift+klik menambah tanpa menghapus",
"ui.selection_shift_add_only_tip": "Aktif: Shift menambah item, Ctrl/Cmd mengalihkan. Mati (default): Shift dan Ctrl/Cmd sama-sama mengalihkan.",
"ui.send": "Kirim",
"ui.send_to_slicer": "Kirim ke slicer",
"ui.sent_to_slicer": "Dikirim ke {slicer}",
"ui.sheet_gap": "Celah",
"ui.shell": "Shell",
@@ -1911,6 +1923,7 @@
"ui.untitled": "Tanpa judul",
"ui.up_axis_note": "Sumbu mana yang mengarah ke atas di tampilan (tampilan sumbu, Home, Look At). Desain tidak berubah.",
"ui.updateToCurrentView": "Perbarui ke Tampilan Saat Ini",
"ui.upload": "Unggah",
"ui.view_centre": "Pusat tampilan",
"ui.visual_shaded_hidden_edges": "Shaded dengan tepi tersembunyi",
"ui.visual_shaded_with_edges": "Shaded dengan tepi",
+20 -5
View File
@@ -462,6 +462,7 @@
"cmd.canvas_delete": "Elimina immagine di riferimento",
"cmd.canvas_edit": "Modifica immagine di riferimento",
"cmd.canvas_insert": "Immagine di riferimento",
"cmd.canvas_to_sketch": "Schizzo da immagine di riferimento",
"cmd.component_activate": "Attiva componente",
"cmd.component_break_link": "Interrompi collegamento",
"cmd.component_capture_position": "Acquisisci posizione",
@@ -539,6 +540,7 @@
"cmd.file_export": "Esporta",
"cmd.file_insert_mesh": "Inserisci mesh",
"cmd.file_insert_step": "Inserisci STEP",
"cmd.file_open_in_easyprint": "Apri con EasyPrint",
"cmd.file_save_mesh": "Salva come mesh",
"cmd.file_slicers": "Slicer installati",
"cmd.frame_member": "Membro strutturale",
@@ -923,7 +925,7 @@
"home.new_mm": "Nuovo progetto",
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
"home.open": "Apri…",
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
"home.open_hint": "Un progetto .sc ({shortcut})",
"home.open_now": "Apri ora",
"home.opened_days_ago": "{count} giorni fa",
"home.opened_hours_ago": "{count} h fa",
@@ -978,6 +980,8 @@
"manage.material_hint": "Seleziona i corpi e scegli il loro materiale fisico.",
"manage.parameters_hint": "Modifica espressioni e unità. Le modifiche vengono applicate premendo Invio o uscendo da un campo.",
"manage.rules_hint": "Scegli una regola da modificare o creane una nuova. Conferma per salvare la regola.",
"material.selection.choose": "Scegli un materiale",
"material.selection.mixed": "Materiali diversi",
"measure.at": "In",
"measure.item": "Elemento {number}",
"messages.ago_h": "{n} h fa",
@@ -1036,6 +1040,12 @@
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Segui un file di tema",
"prefs.theme_follow_error": "Non applicato: {error}. Resta attivo l’ultimo tema valido.",
"prefs.theme_follow_launch": "Seguito solo per questa sessione (--theme-file); ricaricato quando un altro strumento riscrive il file.",
"prefs.theme_follow_note": "Ricaricato quando un altro strumento riscrive il file. Scegliere un tema interrompe il monitoraggio.",
"prefs.theme_follow_off": "Disattivato",
"prefs.theme_follow_stop": "Smetti di seguire",
"properties.title": "Proprietà: {name}",
"recovery.days_ago": "{count} giorni fa",
"recovery.detail": "{file} · {features} feature · salvato automaticamente {when}",
@@ -1365,7 +1375,7 @@
"ui.agent_chat_pop_out": "Apri in una finestra separata, che puoi spostare su un altro schermo",
"ui.agent_chat_previous_picture": "Immagine precedente",
"ui.agent_chat_privacy": "I dati vengono inviati solo all'agente che esegui, con il tuo account.",
"ui.agent_chat_privacy_note": "SolidCraft non invia dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
"ui.agent_chat_privacy_note": "La chat non invia da sola dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
"ui.agent_chat_queue": "Metti in coda",
"ui.agent_chat_queue_full": "Troppi messaggi in attesa: rimuovine uno o attendi l'agente.",
"ui.agent_chat_queued": "In coda",
@@ -1550,6 +1560,7 @@
"ui.command_done": "{command} completato",
"ui.command_palette_hint": "Digita un comando, ad es. {example} — oppure premi S per cercare",
"ui.concentric": "Concentrico",
"ui.cone": "Cono",
"ui.conic_curve": "Curva conica",
"ui.constant": "Costante",
"ui.constraints_conflict": "Vincoli in conflitto",
@@ -1608,6 +1619,11 @@
"ui.draft": "Sformo",
"ui.drawing": "Disegno",
"ui.drill_point": "Punta del trapano",
"ui.easyprint_consent": "EasyPrint è lo slicer di Printables nel browser. Per aprire lì il modello, SolidCraft lo carica sui server di Printables. Il link scade dopo 30 minuti e dovrai aver effettuato l'accesso a Printables. Continuare?",
"ui.easyprint_failed": "Caricamento su EasyPrint non riuscito: {error}",
"ui.easyprint_opened": "Aperto in EasyPrint",
"ui.easyprint_row": "EasyPrint (Printables, nel browser)",
"ui.easyprint_uploading": "Caricamento su Printables…",
"ui.edit": "Modifica",
"ui.editJointLimits": "Modifica limiti del giunto",
"ui.edit_section_analysis": "Modifica analisi in sezione",
@@ -1665,6 +1681,7 @@
"ui.heading_circular_pattern": "RIPETIZIONE CIRCOLARE",
"ui.heading_coil": "ELICA",
"ui.heading_combine": "COMBINA",
"ui.heading_cone": "CONO",
"ui.heading_configure": "CONFIGURA",
"ui.heading_constraints": "VINCOLI",
"ui.heading_construct": "COSTRUISCI",
@@ -1900,9 +1917,7 @@
"ui.right": "Destra",
"ui.save": "Salva",
"ui.save_as": "Salva con nome",
"ui.save_mesh_no_slicer": "Nessuno slicer trovato: scegli Personalizzato… e seleziona lo slicer, oppure disattiva Invia allo slicer",
"ui.save_mesh_pick_bodies": "Seleziona i corpi da salvare",
"ui.save_mesh_temp_failed": "Impossibile creare {folder}: {error}",
"ui.saved_to": "Salvato {path}",
"ui.sbox_pin": "Fissa in cima al riquadro S",
"ui.sbox_unpin": "Sblocca",
@@ -1917,7 +1932,6 @@
"ui.selection_shift_add_only": "Maiusc+clic aggiunge senza rimuovere",
"ui.selection_shift_add_only_tip": "Attivo: Maiusc aggiunge elementi, Ctrl/Cmd commuta. Disattivo (predefinito): Maiusc e Ctrl/Cmd commutano entrambi.",
"ui.send": "Invia",
"ui.send_to_slicer": "Invia allo slicer",
"ui.sent_to_slicer": "Inviato a {slicer}",
"ui.sheet_gap": "Gioco",
"ui.shell": "Svuota",
@@ -2009,6 +2023,7 @@
"ui.untitled": "Senza titolo",
"ui.up_axis_note": "L'asse verticale nelle viste (viste degli assi, Vista iniziale, Vista normale). I progetti restano invariati.",
"ui.updateToCurrentView": "Aggiorna alla vista corrente",
"ui.upload": "Carica",
"ui.view_centre": "Centro della vista",
"ui.visual_shaded_hidden_edges": "Ombreggiato con spigoli nascosti",
"ui.visual_shaded_with_edges": "Ombreggiato con spigoli",

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