Compare commits

..

103 Commits

Author SHA1 Message Date
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
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
271 changed files with 18435 additions and 1293 deletions
+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))
+14 -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 |
@@ -578,6 +578,9 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| 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
Generated
+17 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4242,11 +4242,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.24.1"
version = "0.25.0"
[[package]]
name = "solidcraft-cli"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"image",
"log",
@@ -4258,7 +4258,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"proc-macro2",
"quote",
@@ -4267,7 +4267,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"log",
"serde",
@@ -4284,7 +4284,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"image",
"log",
@@ -4305,7 +4305,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"serde",
"serde_json",
@@ -4314,7 +4314,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"image",
"log",
@@ -4334,7 +4334,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4358,7 +4358,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"log",
"proptest",
@@ -4378,7 +4378,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4387,7 +4387,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"image",
"moxcms",
@@ -4401,7 +4401,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"serde",
"serde_json",
@@ -4412,14 +4412,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"arboard",
"egui",
@@ -4449,7 +4449,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"eframe",
"egui",
@@ -5994,7 +5994,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.24.1"
version = "0.25.0"
dependencies = [
"proc-macro2",
"serde",
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.24.1"
version = "0.25.0"
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.
+7 -1
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>
@@ -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
+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)) };
+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" }
]
}
+37
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> {
@@ -612,6 +627,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]
@@ -671,6 +688,26 @@ 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");
+12 -4
View File
@@ -513,9 +513,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 +546,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 +573,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> {
+17 -7
View File
@@ -417,7 +417,6 @@ impl Lines {
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
_counted: Subscriber,
}
impl Subscription {
@@ -435,10 +434,13 @@ impl Subscription {
let stop = Arc::new(AtomicBool::new(false));
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
let (l, s) = (lines.clone(), stop.clone());
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
// The connection's seat on the watch rides the events thread: it leaves when the thread
// does, so the next failed write to a closed client retires it even while the connection's
// own thread is slow to see the end of the stream (#7226).
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, counted)).ok()?;
let sent = send(&mut &lines.socket, reply);
drop(first);
Some((Subscription { lines, stop, _counted: counted }, sent))
Some((Subscription { lines, stop }, sent))
}
}
@@ -458,8 +460,10 @@ impl Drop for Subscription {
}
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
/// last one it was told of (`seen`), until `stop`.
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
/// last one it was told of (`seen`), until `stop`. Owns the connection's seat on the watch
/// (`counted`), so the seat leaves exactly when this thread does: on `stop`, or on the first
/// write a closed client refuses (#7226).
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), _counted: Subscriber) {
while !stop.load(Ordering::Relaxed) {
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
@@ -866,11 +870,17 @@ mod tests {
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
// Closed, it is no longer counted.
// Closed, it is no longer counted. Each publish asks the events thread for one more line,
// and its write to the closed connection is what retires the seat; that keeps the wait off
// the connection's own thread, which anything else this process is doing may hold up past
// the deadline (#7226).
drop((out, lines));
let mut revision = 11;
let deadline = Instant::now() + Duration::from_secs(10);
while WATCH.subscribed() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
WATCH.publish("d-1", revision);
revision += 1;
std::thread::sleep(Duration::from_millis(20));
}
assert!(!WATCH.subscribed());
}
+125 -17
View File
@@ -92,6 +92,8 @@ struct App {
native_menu: Option<native_menu::NativeMenu>,
/// A device's own app moving the view (Preferences › Navigation, off by default).
nav_bridge: nav_bridge::NavBridge,
/// Where the next file dialog opens: it follows the open design (#7756).
dialog_folder: DialogFolder,
}
impl eframe::App for App {
@@ -122,6 +124,7 @@ impl eframe::App for App {
// Automation runs (a control port was asked for) leave the user's AI apps alone.
if !self.take_focus_unwanted {
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
solidcraft_ui_egui::agent_chat::begin(&mut self.solve);
}
if let Some(dir) = self.starting.take() {
startup::started(&dir);
@@ -146,6 +149,7 @@ impl eframe::App for App {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
self.dialog_folder.design(self.solve.session.path.as_deref());
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
self.space_mouse.tick(&mut self.solve, ctx);
@@ -240,6 +244,60 @@ impl App {
}
}
/// Where the next file dialog opens (#7756): the folder of the file last chosen in one, or of the
/// design last opened or saved, whichever came last. Linux dialogs otherwise open in the home
/// folder every time; macOS and Windows remember a folder of their own, which this agrees with.
#[derive(Clone, Default)]
struct DialogFolder(std::sync::Arc<std::sync::Mutex<DialogFolderState>>);
#[derive(Default)]
struct DialogFolderState {
folder: Option<std::path::PathBuf>,
/// The open design's path when last looked at, to notice another design opened or saved.
design: Option<String>,
}
impl DialogFolder {
/// A file was chosen in a dialog: the next one opens in its folder.
fn chosen(&self, file: &std::path::Path) {
if let (Some(folder), Ok(mut state)) = (file.parent().filter(|f| !f.as_os_str().is_empty()), self.0.lock()) {
state.folder = Some(folder.to_path_buf());
}
}
/// The open design's path, every frame: a design opened or saved somewhere else moves the
/// next dialog to its folder; the same design again leaves a folder chosen since alone.
fn design(&self, path: Option<&str>) {
let Ok(mut state) = self.0.lock() else { return };
if state.design.as_deref() == path {
return;
}
state.design = path.map(str::to_string);
if let Some(folder) = path.and_then(|p| std::path::Path::new(p).parent()).filter(|f| !f.as_os_str().is_empty()) {
state.folder = Some(folder.to_path_buf());
}
}
/// A dialog's answer as a path, its folder remembered for the next dialog.
fn answer(&self, picked: Option<rfd::FileHandle>) -> Option<String> {
let picked = picked?;
self.chosen(picked.path());
Some(picked.path().to_string_lossy().into_owned())
}
fn folder(&self) -> Option<std::path::PathBuf> {
self.0.lock().ok().and_then(|state| state.folder.clone())
}
/// The dialog, opening in the remembered folder when there is one.
fn start(&self, dialog: rfd::AsyncFileDialog) -> rfd::AsyncFileDialog {
match self.folder() {
Some(folder) => dialog.set_directory(folder),
None => dialog,
}
}
}
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
/// case, plus FreeCAD's own mixed casing.
fn case_variants(exts: &[&str]) -> Vec<String> {
@@ -289,15 +347,16 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
raw.safe_area_insets = Some(insets);
}
fn services(cc: &eframe::CreationContext<'_>) -> Services {
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
let program_parent = parent.clone();
let file_parent = parent.clone();
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
Services {
pick_open: Some(Box::new(move |first: &[&str]| {
// The file type the command reads comes first, so the picker opens on it (#1801).
let mut dialog = open_parent.clone();
let mut dialog = open_folder.start(open_parent.clone());
if !first.is_empty() {
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
dialog = dialog.add_filter(label, &case_variants(first));
@@ -310,7 +369,8 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
let future = dialog.add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let folder = open_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
// One kind of file (the agent chat's attachments): only the types it names, as given and in
// lower and upper case (Linux dialogs match case; macOS allows just the listed types).
@@ -325,8 +385,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
}
let all: Vec<&str> = all.iter().map(String::as_str).collect();
// Any other file attaches by reference, so the list is not the only choice.
let future = file_parent.clone().add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let future = file_folder.start(file_parent.clone()).add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
let folder = file_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
pick_program: Some(Box::new(move || {
let dialog = program_parent.clone().set_title("Choose your slicer");
@@ -335,7 +396,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
pick_save: Some(Box::new(move |name: &str, exts: &[&str]| {
let dialog = parent.clone().set_file_name(name);
let dialog = save_folder.start(parent.clone()).set_file_name(name);
let dialog = if exts == [io::DESIGN_EXT, "json"] {
if name.ends_with(".json") {
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
@@ -346,7 +407,8 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
dialog.add_filter("File", exts)
};
let future = dialog.save_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
let folder = save_folder.clone();
Box::pin(async move { folder.answer(future.await) })
})),
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
@@ -553,26 +615,26 @@ fn graphics_path() -> Option<std::path::PathBuf> {
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
/// settings of earlier releases into the new folder. They kept `graphics.json` (and, on Linux
/// and macOS, the recovery folder) under the old product name, and eframe's `app.ron` under the
/// window's old app id. In the Flatpak sandbox, releases before 0.8 kept everything under the
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
/// window's old app id. In the Flatpak sandbox, earlier releases kept everything under an
/// old app id's `~/.var/app` folder; those copies come first, newest first. The old folders
/// are never changed.
fn adopt_legacy_settings() {
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(&current);
if let Err(e) = adopt_settings(&current, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
let old_flatpak = solidcraft_engine::io::legacy_flatpak_paths(&current);
if let Err(e) = adopt_settings(&current, &old_flatpak, &legacy, legacy_eframe.as_deref()) {
log::warn!("settings: could not copy the previous release's settings into {}: {e}", current.display());
}
}
fn adopt_settings(
current: &std::path::Path,
old_flatpak: Option<&std::path::Path>,
old_flatpak: &[std::path::PathBuf],
legacy: &std::path::Path,
legacy_eframe: Option<&std::path::Path>,
) -> std::io::Result<()> {
let fresh = !current.exists();
if let Some(old) = old_flatpak {
for old in old_flatpak {
solidcraft_engine::io::adopt_legacy_dir(current, old)?;
}
solidcraft_engine::io::adopt_legacy_dir(current, legacy)?;
@@ -688,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"));
@@ -712,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.solidcraft | part.step]"
);
return Ok(());
}
_ => files.push(a),
@@ -840,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")]
@@ -863,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(),
@@ -993,6 +1066,7 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
native_menu,
nav_bridge,
dialog_folder,
}))
}),
);
@@ -1171,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")]
@@ -1280,14 +1388,14 @@ mod msaa_tests {
std::fs::create_dir_all(&legacy_eframe).unwrap();
std::fs::write(legacy.join("graphics.json"), r#"{"msaa":8}"#).unwrap();
std::fs::write(legacy_eframe.join("app.ron"), "(prefs)").unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("graphics.json")).unwrap(), r#"{"msaa":8}"#);
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(prefs)");
assert!(legacy.join("graphics.json").is_file() && legacy_eframe.join("app.ron").is_file());
// Later launches leave the current settings alone.
std::fs::write(current.join("app.ron"), "(mine)").unwrap();
std::fs::remove_file(current.join("graphics.json")).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, &[], &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(mine)");
assert!(!current.join("graphics.json").exists());
let _ = std::fs::remove_dir_all(&d);
+7 -2
View File
@@ -146,8 +146,7 @@ impl NativeMenu {
}
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
action.enabled(app)
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
action.menu_enabled(app, ctx.egui_wants_keyboard_input())
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
@@ -157,6 +156,12 @@ impl NativeMenu {
item.set_enabled(enabled);
}
}
for (item, action) in &self.help_actions {
let enabled = solidcraft_ui_egui::help::available(app, action);
if item.is_enabled() != enabled {
item.set_enabled(enabled);
}
}
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
&& let Err(error) = self.update_recent(app)
{
+22 -5
View File
@@ -543,9 +543,16 @@ mod tests {
#[test]
fn probing_a_backend_alone_never_panics() {
// Whatever this machine has (often nothing): an answer, not a crash.
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
// Whatever this machine has (often nothing): an answer, not a crash. On Windows the probe
// goes the way the app's does, stopping at the first backend with a GPU: a machine with
// D3D12 never loads its OpenGL driver into the test process, and AMD's (`atio6axx.dll`)
// aborted the process as it exited (#1156).
if cfg!(target_os = "windows") {
let _ = choose_backends(true, false, false, None, has_hardware_adapter);
} else {
for b in WINDOWS_ORDER {
let _ = has_hardware_adapter(b);
}
}
}
@@ -558,8 +565,18 @@ mod tests {
#[test]
fn the_software_adapter_is_preferred_and_its_absence_is_an_error_not_a_panic() {
assert!(pick_cpu_adapter(&[], None).unwrap_err().contains("none"));
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
let adapters = pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all()));
// The adapters the software renderer chooses among: it starts D3D12 alone on Windows
// (WARP), not every backend, so the test doesn't load the OpenGL driver either (#1156).
let options = crate::wgpu_options(true, None);
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &options.wgpu_setup else { panic!("the software renderer makes its own device") };
let backends = setup.instance_descriptor.backends;
if cfg!(target_os = "windows") && std::env::var_os(WGPU_BACKEND_ENV).is_none() {
assert_eq!(backends, eframe::wgpu::Backends::DX12);
}
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle_from_env();
desc.backends = backends;
let instance = eframe::wgpu::Instance::new(desc);
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
if adapters.is_empty() {
return;
}
+1 -1
View File
@@ -76,7 +76,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `io.tangencylabs.solidcraft` |
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/`, uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
+95 -1
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "إضافة نصف قطر أوسط",
"Add Row": "إضافة صف",
"Add a tool": "إضافة أداة",
"Add all bodies": "إضافة كل الأجسام",
"Add bend": "إضافة ثني",
"Add condition": "إضافة شرط",
"Add or edit rule": "إضافة قاعدة أو تعديلها",
"Add to New Folder": "إضافة إلى مجلد جديد",
"Add visible bodies": "إضافة الأجسام المرئية",
"Advanced": "متقدم",
"Against": "مقابل",
"Alignment": "المحاذاة",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "سطح حدودي",
"BROWSER": "المستعرض",
"Bar": "قضيب",
"Base Center": "مركز القاعدة",
"Bend": "زاوية الثني",
"Bend Edges": "حواف الثني",
"Bend Line Position": "موضع خط الثني",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "إنشاء بلاغ — مراجعة المسودة",
"Create Issue…": "إنشاء بلاغ…",
"Cross curves": "المنحنيات العرضية",
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter يتابع في GitHub.",
"Cube": "مكعب",
"Current orthographic view (1:1 mm)": "العرض المتعامد الحالي (1:1 mm)",
"Current perspective view": "العرض المنظوري الحالي",
@@ -152,6 +156,7 @@
"Customize…": "تخصيص…",
"Cylindrical face": "وجه أسطواني",
"Delete": "حذف",
"Delete Folders and Features…": "حذف المجلدات مع ميزاتها…",
"Delete Selected Features…": "حذف الميزات المحددة…",
"Delete Selected Folders": "حذف المجلدات المحددة",
"Delete all": "حذف الكل",
@@ -430,6 +435,7 @@
"Remove this point": "إزالة هذه النقطة",
"Rename Feature": "إعادة تسمية الميزة",
"Rename Folder": "إعادة تسمية المجلد",
"Render Stage": "مرحلة التصيير",
"Reopen designs": "إعادة فتح التصاميم",
"Reopen the designs that were open when SolidCraft was last closed": "إعادة فتح التصاميم التي كانت مفتوحة عند آخر إغلاق لـ SolidCraft",
"Replace existing library file": "استبدال ملف المكتبة الموجود",
@@ -534,6 +540,7 @@
"Style": "النمط",
"Summary": "الملخص",
"Suppress": "تعطيل",
"Suppress Selected Folders": "تعطيل المجلدات المحددة",
"Suppressed": "مُعطّل",
"Surface": "السطح",
"Surface, or faces to turn over": "السطح، أو الأوجه المراد قلبها",
@@ -581,6 +588,7 @@
"UNTRIM": "إلغاء القص",
"Unit": "الوحدة",
"Unsuppress": "إلغاء التعطيل",
"Unsuppress Selected Folders": "إلغاء تعطيل المجلدات المحددة",
"Updating the preview": "جارٍ تحديث المعاينة",
"Use active author placement": "استخدام موضع المؤلف النشط",
"Use fixed plane": "استخدام مستوى ثابت",
@@ -628,6 +636,8 @@
"ai.prefs.background_commands": "تشغيل أوامر الوكيل في الخلفية",
"ai.prefs.connect_apps": "إتاحة SolidCraft لتطبيقات الذكاء الاصطناعي على هذا الحاسوب",
"ai.prefs.connect_apps_note": "عند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
"ai.prefs.connect_at_start": "الاتصال بآخر وكيل مستخدم عند بدء SolidCraft",
"ai.prefs.connect_at_start_note": "يبدأ في الخلفية الوكيل الذي اتصلت به آخر مرة (أو الوكيل الافتراضي) في كل مرة يبدأ فيها SolidCraft، ليكون جاهزًا للمحادثة. لا يحدث ذلك عند بدء SolidCraft مع --control. إذا تعذّر الاتصال، يعرض المساعد «فشل الاتصال» وزر «إعادة المحاولة».",
"ai.prefs.follow_camera": "توجيه الكاميرا نحو الرسومات",
"ai.prefs.follow_camera_off": "تتبّع الكاميرا التلقائي متوقف.",
"ai.prefs.follow_camera_on": "تتجه الكاميرا نحو الرسومات التي ينشئها الوكيل.",
@@ -635,6 +645,8 @@
"ai.prefs.make_default": "الاستخدام افتراضيًا",
"ai.prefs.no_agents": "لا يوجد وكلاء بعد. أضف وكيلًا أو حدّث القائمة للعثور على الوكلاء المثبّتين.",
"ai.prefs.no_saved_approvals": "لا توجد موافقات محفوظة.",
"ai.prefs.open_at_start": "فتح لوحة المساعد عند بدء SolidCraft",
"ai.prefs.open_at_start_note": "يعرض لوحة المساعد (أو نافذتها المستقلة) في كل مرة يبدأ فيها SolidCraft. لا يحدث ذلك عند بدء SolidCraft مع --control.",
"ai.prefs.override_all_warning": "قد يحفظ هذا الوكيل التصميم أو يكتب فوقه أو يصدّره أو يستبدله دون أن يسأل.",
"ai.prefs.permission_all": "السماح بكل شيء",
"ai.prefs.permission_all_summary": "تعمل أدوات SolidCraft المتاحة دون سؤال.",
@@ -679,6 +691,9 @@
"cmd.appearance_assign": "المظهر",
"cmd.appearance_library": "مكتبة المظاهر",
"cmd.appearance_list": "قائمة المظاهر",
"cmd.appearance_paint": "تلوين المظهر",
"cmd.appearance_pick_up": "التقاط المظهر",
"cmd.appearance_replace": "استبدال المظهر",
"cmd.assembly_bom": "قائمة المواد",
"cmd.body_rename": "إعادة تسمية الجسم",
"cmd.browser_collapse_all": "طي الكل",
@@ -766,6 +781,7 @@
"cmd.document_revision": "مراجعة التصميم",
"cmd.document_units": "تغيير وحدات التصميم",
"cmd.edit_checkpoint": "نقطة حفظ",
"cmd.edit_repeat": "تكرار آخر أمر",
"cmd.edit_restore_checkpoint": "استعادة نقطة الحفظ",
"cmd.engine_commands": "قائمة الأوامر",
"cmd.engine_describe": "وصف الأمر",
@@ -879,9 +895,11 @@
"cmd.plastic_snap_fit": "وصلة كبس",
"cmd.render_aging_describe": "تقادم السطح بالطبقات",
"cmd.render_backends": "محركات التصيير",
"cmd.render_batch": "تصيير كل اللقطات",
"cmd.render_camera_fit": "تأطير موضوع التصيير",
"cmd.render_camera_get": "فحص كاميرا التصيير",
"cmd.render_cancel": "إلغاء التصيير",
"cmd.render_compare": "مقارنة الصور",
"cmd.render_effects_describe": "تأثيرات العدسة والتأثيرات الإبداعية",
"cmd.render_export": "تصدير صورة التصيير",
"cmd.render_hdr_inspect": "فحص مصدر HDR",
@@ -897,6 +915,7 @@
"cmd.render_library_surface_prepare": "تجهيز خرائط سطح مُعدّة",
"cmd.render_library_surface_unbind": "إزالة ربط السطح",
"cmd.render_library_targets": "البحث عن أهداف مكتبة التصيير",
"cmd.render_lights_solo": "عزل الضوء",
"cmd.render_optics_bind": "حفظ الربط البصري",
"cmd.render_optics_unbind": "إزالة الربط البصري",
"cmd.render_preflight": "فحص طلب التصيير",
@@ -1006,7 +1025,9 @@
"cmd.sketch_spline_control_point": "منحنى شريحي بنقاط تحكم",
"cmd.sketch_spline_control_point_5": "منحنى شريحي بنقاط تحكم (الدرجة 5)",
"cmd.sketch_spline_fit_point": "منحنى شريحي بنقاط ملاءمة",
"cmd.sketch_spline_insert_point": "إضافة نقطة إلى المنحنى الشريحي",
"cmd.sketch_spun_profile": "مقطع دوراني",
"cmd.sketch_toggle_diameter": "تبديل نصف القطر / القطر",
"cmd.sketch_toggle_driven": "تبديل البُعد المُشتق",
"cmd.sketch_transform": "تحويل",
"cmd.sketch_trim_path": "قص على طول مسار",
@@ -1072,6 +1093,7 @@
"cmd.view_rename": "إعادة تسمية عرض مسمّى",
"cmd.view_right": "العرض الأيمن",
"cmd.view_save": "عرض مسمّى جديد",
"cmd.view_shots_strip": "شريط اللقطات",
"cmd.view_spin": "تدوير مستمر",
"cmd.view_tool": "أداة التنقل",
"cmd.view_top": "العرض العلوي",
@@ -1145,6 +1167,7 @@
"document.save_changes": "حفظ التغييرات على {name}؟",
"document.save_copy": "حفظ نسخة…",
"document.unsaved_changes": "في {name} تغييرات غير محفوظة. إن لم تحفظها فستضيع.",
"drive.rigid": "{joint} مفصل صلب: يثبّت أجزاءه في مكانها، لذا لا يوجد ما يمكن تحريكه. لتتحرك الأجزاء، غيّر نوعه في «تعديل المفصل»: Revolute للدوران، Slider للانزلاق، Cylindrical لكليهما.",
"drive.stopped_by_contact": "توقف عند {value}: سيخترق {a} الجسم {b}",
"drive.stopped_by_loop": "توقف عند {value}: لا يمكن للحلقة عبر {joint} أن تنغلق أكثر",
"drive.unconnected": "لا يحرّك شيئًا: {part} غير متصل بأي شيء مثبت بالأرض",
@@ -1286,6 +1309,9 @@
"install.steps_intro": "يثبّت Safari تطبيقات الويب من قوائمه الخاصة:",
"install.title": "تثبيت SolidCraft",
"issue.body_label": "نص البلاغ",
"issue.note_changed_while_waiting": "تغيّرت الإعدادات أو النموذج أثناء انتظار المعاينة. أعد التقاط الشاشة لتحديث التفاصيل التقنية للعرض الحالي.",
"issue.note_details_kept": "التُقطت لقطة الشاشة محليًا. احتُفظ بالتفاصيل التقنية التي عدّلتها: تحقّق من أنها لا تزال تطابق العرض الحالي.",
"issue.note_details_updated": "حُدّثت التفاصيل التقنية لتطابق العرض الحالي. التُقطت لقطة الشاشة محليًا. راجعها قبل إرفاقها في GitHub.",
"issue.note_image_copied": "الصق الصورة في مسودة GitHub، أو احفظ ملف PNG إن لم يتوفر نسخ الصور.",
"issue.note_opened": "راجع مسودة GitHub وأرفق لقطة الشاشة. زر Submit new issue في GitHub ينشرها.",
"issue.note_opened_copied": "نُسخ البلاغ. الصقه في مسودة GitHub، وأرفق لقطة الشاشة، ثم انقر Submit new issue في GitHub.",
@@ -1352,6 +1378,8 @@
"material.editor.unknown_card": "الخصائص الهندسية غير معروفة. قد يحتفظ الحفظ بالكثافة المعيّنة والمظهر الحالي فقط.",
"material.editor.user_supplied": "من المستخدم",
"material.editor.wood": "خشب",
"material.selection.choose": "اختر مادة",
"material.selection.mixed": "مواد مختلفة",
"measure.at": "عند",
"measure.item": "العنصر {number}",
"messages.ago_h": "قبل {n} ساعة",
@@ -1381,6 +1409,8 @@
"nav.free_orbit.help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. يُعطَّل التقاط المحاور بـ Shift.",
"nav.free_orbit.label": "تدوير حر",
"nav.free_orbit.snap_help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. اضغط مطولًا على Shift أثناء سحب التدوير لالتقاط اتجاه العرض واللف.",
"nav.free_orbit_upright.help": "سحب الفأرة جانبيًا يدوّر النموذج حول المحور الرأسي للعالم؛ والسحب لأعلى ولأسفل يميله فوق القمة دون حد.",
"nav.free_orbit_upright.label": "الدوران حول المحور الرأسي",
"nav.fusion.help": "السحب بالزر الأوسط يُزيح العرض، وShift مع الأوسط يدوّره، والسحب بالأيمن يدوّره",
"nav.inventor.help": "السحب بالزر الأوسط يُزيح العرض، وShift مع الأوسط يدوّره، وCtrl مع الأوسط يكبّره، والسحب بالأيمن يدوّره",
"nav.openscad.help": "السحب بالزر الأيسر على مساحة فارغة يدوّر العرض، وShift مع السحب بالأيسر يحدد بمربع، وسحب هندسة الرسم يعدّلها؛ والسحب بالأيمن أو الأوسط يُزيح، وShift مع السحب بالأيمن أو الأوسط يكبّر",
@@ -1456,6 +1486,10 @@
"prefs.design_note": "ما تبدأ به التصاميم الجديدة. التصاميم المفتوحة تحتفظ بإعداداتها.",
"prefs.devices": "الفأرة ثلاثية الأبعاد",
"prefs.dialog_buttons": "أزرار مربعات الحوار",
"prefs.dialogs": "الحوارات",
"prefs.dialogs_edge": "عند حافة العرض",
"prefs.dialogs_left_column": "في العمود الأيسر",
"prefs.dialogs_note": "مكان فتح حوارات الأوامر: مثبتة عند حافة العرض، أو في العمود الأيسر مكان المستعرض والسجل، اللذين يعودان كما كانا عند إغلاق الحوار.",
"prefs.display": "العرض",
"prefs.edges": "الحواف والتظليل",
"prefs.extend": "توسيع",
@@ -1515,6 +1549,8 @@
"prefs.opaque": "معتم",
"prefs.orbit_snap": "اضغط Shift أثناء التدوير للالتقاط على محور.",
"prefs.orientation": "الاتجاه",
"prefs.origin_plane_names": "أسماء مستويات الأصل",
"prefs.origin_plane_names_note": "الأسماء المعروضة في المستعرض والحوارات والقوائم. {top} هو XY و{front} هو XZ و{right} هو YZ، والمحور Z للأعلى. تحتفظ الأوامر والسكربتات والتصاميم المحفوظة بـ XY وXZ وYZ.",
"prefs.overall_speed": "السرعة العامة",
"prefs.placement": "الموضع",
"prefs.prefer_free_sketch_points": "تفضيل نقاط الرسم القابلة للتحريك عند تداخل النقاط",
@@ -1555,6 +1591,12 @@
"prefs.supersampling_brief": "يرسم العرض بحجم أكبر ثم يصغّره: حاد جدًا لكنه أثقل.",
"prefs.tab_accent_note": "لون التمييز يتبع التبويب النشط",
"prefs.theme_file": "ملف السمة",
"prefs.theme_follow": "اتباع ملف سمة",
"prefs.theme_follow_error": "لم يُطبّق: {error}. تبقى آخر سمة سليمة مفعّلة.",
"prefs.theme_follow_launch": "يُتّبع لهذه الجلسة فقط (--theme-file)؛ يُعاد تحميله كلما أعادت أداة أخرى كتابته.",
"prefs.theme_follow_note": "يُعاد تحميله كلما أعادت أداة أخرى كتابته. اختيار سمة يوقف اتباعه.",
"prefs.theme_follow_off": "إيقاف",
"prefs.theme_follow_stop": "إيقاف الاتباع",
"prefs.timeline_half_hour": "−30 دقيقة",
"prefs.timeline_now": "الآن",
"prefs.timeline_quarter_hour": "−15",
@@ -1619,6 +1661,7 @@
"render.aging_target_search": "تصفية أسماء الأجسام",
"render.aging_upward": "ترسّب على الأسطح المتجهة للأعلى",
"render.all_controls_help": "فحص مكثف لكل عناصر التحكم الحالية. صفحات الأقسام توفر تحريرًا مركّزًا دون تغيير الطلب الأساسي.",
"render.all_shots_label": "كل اللقطات",
"render.appearance_physics_separate": "المظهر المرئي لا يعيّن الكثافة ولا البيانات الحرارية ولا أي خصائص هندسية أخرى.",
"render.availability_native": "تركيب أصلي؛ راجعه قبل التطبيق",
"render.availability_projection": "إسقاط مدعوم؛ راجع النطاق وما أُغفل",
@@ -1773,10 +1816,13 @@
"render.effects_reset_lens": "إعادة ضبط تأثيرات العدسة",
"render.effects_seed": "بذرة النمط",
"render.export_display": "تصدير: PNG للعرض",
"render.export_image_label": "تصدير الصورة",
"render.export_linear": "تصدير: EXR خطي خام",
"render.exr_pending": "يستطيع GPU الاحتفاظ بـ EXR خطي خام. الضغط والصور متعددة الطبقات وتمريرات التصيير المساعدة غير متاحة بعد.",
"render.film_grain": "حبيبات فوتوغرافية",
"render.fit": "ملاءمة المنتج",
"render.fixed_camera": "الكاميرا",
"render.fixed_world": "العالم",
"render.flare_ghosts": "حلقات الوهج الشبحية لكل مصدر",
"render.flare_halo": "هالة الوهج",
"render.flare_streak": "خط ضوئي بأسلوب أنامورفي",
@@ -1803,6 +1849,12 @@
"render.hdr_primaries_srgb": "الألوان الأساسية: sRGB/D65",
"render.hdr_primaries_undeclared": "لا يصرّح المصدر بالألوان الأساسية؛ راجع متطلبات الألوان لدى المُصيّر.",
"render.hdr_resource_missing": "مورد HDR المحفوظ مفقود من هذا التصميم.",
"render.heading_batch": "الدفعة",
"render.heading_frame": "الإطار",
"render.heading_lens": "العدسة",
"render.heading_results": "النتائج",
"render.heading_shots": "اللقطات",
"render.hide_shots_label": "إخفاء شريط اللقطات",
"render.image_advanced": "عناصر تحكم GPU المتقدمة",
"render.image_cancelled": "أُلغي التصيير",
"render.image_cancelling": "جارٍ الإلغاء",
@@ -1834,6 +1886,7 @@
"render.image_width": "العرض",
"render.include_rig_cards": "تضمين البطاقات والأعلام السلبية",
"render.inspect_scene": "تحديث ملخص المشهد الملتقط",
"render.inspector": "المفتش",
"render.keep_editing": "متابعة التحرير",
"render.layer_down": "نقل للأسفل",
"render.layer_enabled": "الطبقة مفعّلة",
@@ -1902,6 +1955,8 @@
"render.library_unfavorite": "إزالة من المفضلة",
"render.library_uniform_ack": "استخدام التقريب الموحّد دون تأثيرات مكانية أو متقدمة",
"render.library_usable": "إسقاطات قابلة للتنفيذ",
"render.light_anchor_pending": "تغيير نظام إحداثيات الضوء غير مدعوم بعد.",
"render.lights_fixed_to": "الأضواء مثبّتة على",
"render.linear_help": "يحتفظ بنسخة أصلية RGBA بفاصلة عائمة 32 بت قبل إزالة التشويش والتعريض وتخطيط الدرجات والمرشحات والخلفية. sRGB/D65 خطي؛ مضروب مسبقًا في التغطية. تُقبل الذاكرة الإضافية صراحةً. بدون ضغط أو تمريرات إضافية.",
"render.linear_retain": "الاحتفاظ بـ EXR خطي خام",
"render.linear_unavailable": "الإطار المكتمل المختار ليس له EXR محفوظ. فعّل الاحتفاظ قبل التصيير؛ لا يستطيع PNG أقدم استعادة الإشعاع الخام.",
@@ -1987,6 +2042,7 @@
"render.output_over_budget": "الصورة المطلوبة تتجاوز نطاق الموارد المختار. اختر حجمًا أو ملفًا متوافقًا صراحةً؛ لا يحدث تخفيض تلقائي.",
"render.output_ratio_refused": "البُعد المرتبط يتجاوز حد الحجم لهذه الطريقة. يُحتفظ بالحجم السابق؛ ألغِ قفل النسبة أو اختر قيمة أصغر.",
"render.output_swap_orientation": "تبديل العرض والارتفاع",
"render.output_tools_inspector": "عدّل الحجم والصيغة في مفتش المخرجات.",
"render.page_aging": "التقادم والتلوث",
"render.page_all": "كل عناصر التحكم",
"render.page_appearance": "المظهر",
@@ -1998,6 +2054,7 @@
"render.page_lighting": "الإضاءة",
"render.page_output": "الإخراج",
"render.page_scene": "المشهد",
"render.paint_label": "تلوين",
"render.passive_stage": "الأرضية والمنصة السلبية",
"render.passive_stage_help": "الألواح السلبية المحدودة تخص الصور المُصيّرة فقط. لا تضيف أجسام CAD ولا كتلة. الموضع ثابت بعد التطبيق؛ وتقاطع حدود المنتج يُرفض. لا يوفر CPU إضاءة ارتداد منتشرة.",
"render.perspective": "كاميرا منظورية",
@@ -2013,6 +2070,10 @@
"render.physical_prepare_plan": "تجهيز خطة الإنشاء (دون تطبيق)",
"render.physical_separate": "المظهر ليس تعريفًا للمادة. هذه مراجع موثقة المصدر، لا خصائص فعلية معتمدة. تبقى الكتلة والمواد الهندسية والبصريات الحالية دون تغيير.",
"render.physical_unknown": "لم يثبت أي مرشح رقمي. يجب تحديد الدرجة أو التركيبة أو البنية؛ وغير المعروف لا يعني صفرًا.",
"render.pick_component": "مكوّن",
"render.pick_label": "التقاط",
"render.pick_up_label": "التقاط المظهر",
"render.pick_waiting": "اختيار المنتج غير مدعوم بعد.",
"render.post_contrast": "التباين",
"render.post_gain_b": "كسب الأزرق",
"render.post_gain_g": "كسب الأخضر",
@@ -2025,6 +2086,7 @@
"render.preview": "المعاينة المدمجة",
"render.preview_help": "تظليل نقطي وشفافية ممزوجة. الانكسار وHDRI والتصيير النهائي غير متاحة بعد.",
"render.preview_image_settings_help": "تحافظ المعاينة الحية على إضاءة استوديو ثابتة. يُطبّق التعريض وإضاءة الصورة على مهمة الصورة عند «فحص» أو «بدء».",
"render.preview_lighting_help": "المعاينة الحية تعرض الضوء المباشر فقط وبلا ظلال.",
"render.preview_retry": "إعادة محاولة المعاينة الحية",
"render.preview_unavailable_help": "المعاينة الحية غير متاحة. تبقى الإعدادات المحفوظة وعناصر التحكم في مهمة الصورة متاحة. غيّر المشهد أو أعد المحاولة صراحةً؛ لا يُعاد بناء المشهد المرفوض في كل إطار.",
"render.preview_upload": "حمولة رؤوس المعاينة الحية",
@@ -2038,8 +2100,10 @@
"render.radial_distortion": "تشوه شعاعي",
"render.regrade_action": "إعادة تدريج النسخة الخام (دون إزالة تشويش)",
"render.regrade_help": "ينشئ PNG منفصلًا من النسخة الخام المكتملة المختارة باستخدام عناصر التحكم هذه في العرض. لا يتتبع أشعة، ولا يغيّر الحجم أو الكاميرا أو الأضواء الملتقطة، ولا يستبدل EXR. لا يُعاد تطبيق إزالة التشويش: النسخ الخام لا تحتفظ بأدلتها. يبقى الناتج المشتق قابلًا للتصدير بعناصر التحكم نفسها في المهمة.",
"render.regrade_label": "إعادة تدرّج",
"render.reload_settings": "إعادة تحميل الإعدادات المحفوظة",
"render.replace_ground": "تأكيد استبدال المنصة الحالية",
"render.replace_label": "استبدال",
"render.replace_saved_surface": "استبدال ربط محفوظ موجود لهذا الهدف",
"render.restore_camera": "استعادة الكاميرا المحفوظة",
"render.result_actual_help": "بكسل صورة واحد لكل بكسل عرض.",
@@ -2085,6 +2149,7 @@
"render.sampling_title": "العيّنات التكيّفية وعيّنات النفاذ",
"render.save_continue": "الحفظ والمتابعة",
"render.save_setup": "حفظ الإعداد",
"render.save_shot": "حفظ اللقطة",
"render.saved_maps_runtime_conflict": "لا يمكن خلط روابط الأسطح المحفوظة مع تجاوزات بصرية مؤقتة أو HDR غير محفوظ. احفظ HDR أو أزل المدخلات المؤقتة صراحةً.",
"render.saved_surface_remove": "إزالة الربط المحفوظ",
"render.saved_surfaces": "روابط الأسطح المحفوظة",
@@ -2104,6 +2169,9 @@
"render.setup_runtime_unsupported": "لا يمكن حفظ هذه المهمة الكاملة بعد: فيها خرائط أو روابط لأجسام المشهد. أزل تلك المدخلات المؤقتة قبل حفظ إعداد قابل للنقل.",
"render.setup_save_missing": "تعذّر العثور على إعداد التصيير المحفوظ بعد اكتمال الأمر.",
"render.sharpening": "زيادة حدة التفاصيل",
"render.shell_tab_appearance": "مظهر التصيير",
"render.shell_tab_camera": "كاميرا التصيير",
"render.shell_tab_scene": "مشهد التصيير",
"render.shot_duplicate": "تكرار اللقطة المحفوظة",
"render.shot_manager": "اللقطات المحفوظة",
"render.shot_new_name": "اسم اللقطة الجديد",
@@ -2112,11 +2180,19 @@
"render.shot_remove_title": "إزالة اللقطة المحفوظة؟",
"render.shot_rename": "إعادة تسمية اللقطة المحفوظة",
"render.shot_saved_only": "احفظ المسودة الحالية أولًا. التكرار وإعادة التسمية يحافظان على الخيارات المحفوظة ومراجع الموارد بالضبط.",
"render.show_shots_label": "إظهار شريط اللقطات",
"render.sky_ground": "الأرض · RGB خطي",
"render.sky_horizon": "الأفق · RGB خطي",
"render.sky_zenith": "السمت · RGB خطي",
"render.soft_focus": "تركيز ناعم",
"render.stage_frame": "الإطار",
"render.stage_light": "الضوء",
"render.stage_look": "المظهر",
"render.stage_orthographic": "إسقاط متعامد",
"render.stage_output": "المخرجات",
"render.stage_panel_count": "الألواح المحفوظة",
"render.stage_preview_note": "معاينة مدمجة · ضوء مباشر",
"render.strip_label": "الشريط",
"render.studio_add_area": "إضافة ضوء مساحي",
"render.studio_ambient": "إشعاع البيئة الثابت",
"render.studio_ambient_rgb": "إشعاع البيئة الثابت (RGB خطي)",
@@ -2173,6 +2249,12 @@
"render.volume_max": "الحدود القصوى · mm العالم",
"render.volume_min": "الحدود الدنيا · mm العالم",
"render.volume_title": "حجم متجانس محدود",
"render.wait_batch": "غير مدعوم بعد: قائمة تصيير اللقطات المحفوظة",
"render.wait_compare": "غير مدعوم بعد: مقارنة الصور المحتفظ بها",
"render.wait_paint": "غير مدعوم بعد: اختيار المنتج وتلوين المظهر",
"render.wait_pick_up": "غير مدعوم بعد: اختيار المنتج وأخذ عينة من المظهر",
"render.wait_replace": "غير مدعوم بعد: استبدال كل استخدامات المظهر",
"render.wait_solo": "غير مدعوم بعد: ظهور الأضواء وعزلها",
"sample.bracket.about": "شفة أساسية مع حواف مثنية وثنية حافة وفق قاعدة 2 mm",
"sample.bracket.name": "حامل من صفائح معدنية",
"sample.enclosure.about": "صندوق مجوّف بشفة ونتوءات للبراغي ومسند ووصلة كبس",
@@ -2264,6 +2346,7 @@
"timeline.delete_after_marker_one": "حذف ميزة واحدة بعد مؤشر السجل",
"timeline.delete_after_marker_other": "حذف {count} ميزات بعد مؤشر السجل",
"timeline.delete_folders_tip": "الإبقاء على ميزاتها ومجلداتها الفرعية في السجل",
"timeline.delete_folders_with_features_tip": "حذف المجلدات مع كل الميزات التي فيها والمجلدات المتداخلة فيها",
"timeline.delete_many": "حذف {count} ميزات محددة",
"timeline.feature_error": "خطأ: {message}",
"timeline.feature_hint": "انقر نقرًا مزدوجًا للتحرير · اسحب لإعادة الترتيب · انقر بالزر الأيمن للمزيد",
@@ -2273,6 +2356,7 @@
"timeline.move_to_group": "نقل الميزات إلى مجلد",
"timeline.rename_group": "إعادة تسمية مجلد الميزات",
"timeline.reorder_many": "إعادة ترتيب الميزات",
"timeline.suppress_folders_tip": "تعطيل كل الميزات في المجلدات والمجلدات المتداخلة فيها أو إلغاء تعطيلها",
"timeline.ungroup": "إزالة مجلدات الميزات",
"to the face": "إلى الوجه",
"to the next face": "إلى الوجه التالي",
@@ -2283,7 +2367,11 @@
"toolbar.render_camera": "الكاميرا",
"toolbar.render_environment": "البيئة",
"toolbar.render_lighting": "الإضاءة",
"toolbar.render_lights": "الأضواء",
"toolbar.render_result": "النتيجة",
"toolbar.render_scene": "المشهد",
"toolbar.render_stage": "المرحلة",
"toolbar.render_studio": "الاستوديو",
"trace.points": "{count} نقطة على طول المسار",
"trace.stopped_by_contact": "ينتهي عند العيّنة {sample}: سيخترق {a} الجسم {b}",
"trace.stopped_by_loop": "ينتهي عند العيّنة {sample}: لا يمكن للحلقة عبر {joint} أن تنغلق أكثر",
@@ -2462,6 +2550,7 @@
"ui.agent_chat_import_failed": "{name}: فشل الاستيراد: {error}",
"ui.agent_chat_intro": "تحدّث مع وكيل برمجي تستخدمه أصلًا، أو مع نقطة نهاية نموذج تختارها. يعمل على التصميم المفتوح عبر أدوات SolidCraft: تظهر تغييراته أثناء إجرائها، وتُلغى بخطوة تراجع واحدة لكل رسالة. ما يمكنه فعله دون سؤال يُضبط في التفضيلات › الذكاء الاصطناعي.",
"ui.agent_chat_key_in_env": "المفتاح مضبوط",
"ui.agent_chat_last_missing": "الوكيل المستخدم آخر مرة ({agent}) لم يعد متاحًا. اختر وكيلًا، ثم اضغط «إعادة المحاولة».",
"ui.agent_chat_left": "يسار",
"ui.agent_chat_limit": "بلغ الوكيل حد {limit} تغييرات للرسالة الواحدة. أرسل رسالة أخرى ليتابع.",
"ui.agent_chat_login_found": "تم العثور على تسجيل دخول",
@@ -2508,6 +2597,7 @@
"ui.agent_chat_remove": "إزالة من قائمة الانتظار",
"ui.agent_chat_remove_file": "إزالته من هذه الرسالة",
"ui.agent_chat_remove_imported": "إزالته من هذه الرسالة؛ تبقى القطعة في التصميم (التراجع يزيلها)",
"ui.agent_chat_retry": "إعادة المحاولة",
"ui.agent_chat_right": "يمين",
"ui.agent_chat_save": "حفظ",
"ui.agent_chat_send": "إرسال",
@@ -2520,6 +2610,7 @@
"ui.agent_chat_state_auth": "يلزم تسجيل الدخول",
"ui.agent_chat_state_busy": "جارٍ العمل…",
"ui.agent_chat_state_exited": "توقف الوكيل",
"ui.agent_chat_state_failed": "فشل الاتصال",
"ui.agent_chat_state_off": "غير متصل",
"ui.agent_chat_state_ready": "جاهز",
"ui.agent_chat_state_starting": "جارٍ البدء…",
@@ -2823,6 +2914,7 @@
"ui.fully_constrained": "مقيّد بالكامل",
"ui.function": "دالة",
"ui.general": "عام",
"ui.gpu_memory_lowered": "نفدت ذاكرة بطاقة الرسومات: يُرسم العرض بجودة أقل.",
"ui.graphics": "الرسوميات",
"ui.graphics_custom": "مخصص",
"ui.graphics_performance": "الأداء",
@@ -3093,6 +3185,7 @@
"ui.render": "التصيير",
"ui.renderer": "محرك التصيير",
"ui.repeat": "تكرار",
"ui.repeat_nothing": "لا يوجد ما يُكرَّر بعد: ابدأ أداة أو مربع حوار أولًا.",
"ui.replace_with_link_done": "أصبحت النسخ الآن مواضع للتصميم المرتبط.",
"ui.report_a_bug": "الإبلاغ عن خطأ",
"ui.reset": "إعادة تعيين",
@@ -3315,5 +3408,6 @@
"web.open_link": "فتح التصميم المرتبط؟",
"web.open_link_confirm": "توجد تغييرات غير محفوظة. هل تريد فتح التصميم المرتبط في تبويب جديد؟",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ اختيار · Enter / Tab إكمال · Esc إغلاق",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ اختيار · Enter يطبّق ما كتبته · Esc إغلاق"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ اختيار · Enter يطبّق ما كتبته · Esc إغلاق",
"⌘Enter continues on GitHub.": "⌘Enter يتابع في GitHub."
}
+6
View File
@@ -1016,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}",
+6
View File
@@ -1324,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",
+308 -1
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",
@@ -571,6 +703,7 @@
"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",
@@ -635,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",
@@ -657,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",
@@ -666,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",
@@ -710,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",
@@ -739,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",
@@ -750,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",
@@ -777,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",
@@ -788,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",
@@ -852,7 +1008,9 @@
"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",
@@ -900,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",
@@ -947,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",
@@ -962,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",
@@ -979,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...",
@@ -991,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",
@@ -1067,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.",
@@ -1078,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",
@@ -1088,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.",
@@ -1119,6 +1363,8 @@
"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",
@@ -1148,9 +1394,21 @@
"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.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.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}",
@@ -1159,6 +1417,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.",
@@ -1180,8 +1439,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",
@@ -1205,6 +1473,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",
@@ -1240,6 +1509,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)",
@@ -1249,6 +1520,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",
@@ -1261,9 +1534,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.",
@@ -1290,15 +1570,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",
@@ -1372,12 +1670,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",
@@ -1388,7 +1688,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",
@@ -1876,6 +2180,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",
@@ -2118,6 +2423,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",
@@ -2311,5 +2617,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.",
+95 -1
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,7 @@
"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",
@@ -534,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",
@@ -581,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",
@@ -628,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.",
@@ -635,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.",
@@ -679,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",
@@ -766,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",
@@ -879,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",
@@ -897,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",
@@ -1006,7 +1025,9 @@
"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",
@@ -1072,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",
@@ -1145,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",
@@ -1286,6 +1309,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.",
@@ -1352,6 +1378,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",
@@ -1381,6 +1409,8 @@
"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",
@@ -1456,6 +1486,10 @@
"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",
@@ -1515,6 +1549,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",
@@ -1555,6 +1591,12 @@
"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",
@@ -1619,6 +1661,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",
@@ -1773,10 +1816,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",
@@ -1803,6 +1849,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",
@@ -1834,6 +1886,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",
@@ -1902,6 +1955,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.",
@@ -1987,6 +2042,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",
@@ -1998,6 +2054,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",
@@ -2013,6 +2070,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",
@@ -2025,6 +2086,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",
@@ -2038,8 +2100,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.",
@@ -2085,6 +2149,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",
@@ -2104,6 +2169,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",
@@ -2112,11 +2180,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)",
@@ -2173,6 +2249,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",
@@ -2264,6 +2346,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",
@@ -2273,6 +2356,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",
@@ -2283,7 +2367,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",
@@ -2462,6 +2550,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",
@@ -2508,6 +2597,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",
@@ -2520,6 +2610,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…",
@@ -2823,6 +2914,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",
@@ -3093,6 +3185,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",
@@ -3315,5 +3408,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."
}
+7
View File
@@ -759,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",
@@ -1089,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}",
+1161 -1
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -1003,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}",
+95 -1
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "הוספת רדיוס ביניים",
"Add Row": "הוספת שורה",
"Add a tool": "הוספת כלי",
"Add all bodies": "הוספת כל הגופים",
"Add bend": "הוספת כיפוף",
"Add condition": "הוספת תנאי",
"Add or edit rule": "הוספה או עריכה של כלל",
"Add to New Folder": "הוספה לתיקייה חדשה",
"Add visible bodies": "הוספת הגופים הגלויים",
"Advanced": "מתקדם",
"Against": "מול",
"Alignment": "יישור",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "משטח גבול",
"BROWSER": "עץ התכן",
"Bar": "מוט",
"Base Center": "מרכז הבסיס",
"Bend": "זווית כיפוף",
"Bend Edges": "מקצועות כיפוף",
"Bend Line Position": "מיקום קו הכיפוף",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "יצירת דיווח — בדיקת הטיוטה",
"Create Issue…": "יצירת דיווח…",
"Cross curves": "עקומות חוצות",
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter ממשיך ב־GitHub.",
"Cube": "קובייה",
"Current orthographic view (1:1 mm)": "התצוגה האורתוגרפית הנוכחית (1:1 mm)",
"Current perspective view": "תצוגת הפרספקטיבה הנוכחית",
@@ -152,6 +156,7 @@
"Customize…": "התאמה אישית…",
"Cylindrical face": "פאה גלילית",
"Delete": "מחיקה",
"Delete Folders and Features…": "מחיקת תיקיות עם הפיצ'רים שבהן…",
"Delete Selected Features…": "מחיקת הפיצ'רים שנבחרו…",
"Delete Selected Folders": "מחיקת התיקיות שנבחרו",
"Delete all": "מחיקת הכול",
@@ -430,6 +435,7 @@
"Remove this point": "הסרת נקודה זו",
"Rename Feature": "שינוי שם פיצ'ר",
"Rename Folder": "שינוי שם תיקייה",
"Render Stage": "שלב הרינדור",
"Reopen designs": "פתיחה מחדש של תכנים",
"Reopen the designs that were open when SolidCraft was last closed": "פתיחה מחדש של התכנים שהיו פתוחים כש־SolidCraft נסגרה בפעם האחרונה",
"Replace existing library file": "החלפת קובץ הספרייה הקיים",
@@ -534,6 +540,7 @@
"Style": "סגנון",
"Summary": "סיכום",
"Suppress": "השבתה",
"Suppress Selected Folders": "השבתת התיקיות שנבחרו",
"Suppressed": "מושבת",
"Surface": "משטח",
"Surface, or faces to turn over": "משטח, או פאות להיפוך",
@@ -581,6 +588,7 @@
"UNTRIM": "ביטול גיזום",
"Unit": "יחידה",
"Unsuppress": "ביטול השבתה",
"Unsuppress Selected Folders": "ביטול השבתת התיקיות שנבחרו",
"Updating the preview": "התצוגה המקדימה מתעדכנת",
"Use active author placement": "שימוש במיקום היצירה הפעיל",
"Use fixed plane": "שימוש במישור קבוע",
@@ -628,6 +636,8 @@
"ai.prefs.background_commands": "הרצת פקודות הסוכן ברקע",
"ai.prefs.connect_apps": "הנגשת SolidCraft ליישומי בינה מלאכותית במחשב זה",
"ai.prefs.connect_apps_note": "בהפעלת SolidCraft הוא מתווסף כשרת MCP ליישומי הבינה המלאכותית שנמצאו במחשב זה (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), כך שסוכן יכול להשתמש בו בלי שום הגדרה. רק הרשומה solidcraft נוספת או שהנתיב שלה מתעדכן; רשומה ששינית נשארת כפי שהיא, ולפני כן נשמר גיבוי של הקובץ. כבוי: הרשומות שנוספו מוסרות. הפקודה עזרה > חיבור עוזרי בינה מלאכותית… עושה זאת ידנית.",
"ai.prefs.connect_at_start": "התחברות לסוכן האחרון שבו השתמשת בהפעלת SolidCraft",
"ai.prefs.connect_at_start_note": "מפעיל ברקע את הסוכן שאליו התחברת לאחרונה (או את סוכן ברירת המחדל) בכל הפעלה של SolidCraft, כך שהוא מוכן לשיחה. לא כש-SolidCraft מופעל עם --control. אם ההתחברות נכשלת, העוזר מציג „ההתחברות נכשלה” ולחצן „ניסיון חוזר”.",
"ai.prefs.follow_camera": "הפניית המצלמה אל סקיצות",
"ai.prefs.follow_camera_off": "מעקב המצלמה האוטומטי כבוי.",
"ai.prefs.follow_camera_on": "המצלמה פונה אל הסקיצות שהסוכן יוצר.",
@@ -635,6 +645,8 @@
"ai.prefs.make_default": "שימוש כברירת מחדל",
"ai.prefs.no_agents": "אין עדיין סוכנים. אפשר להוסיף סוכן או לרענן כדי למצוא סוכנים מותקנים.",
"ai.prefs.no_saved_approvals": "אין אישורים שמורים.",
"ai.prefs.open_at_start": "פתיחת חלונית העוזר בהפעלת SolidCraft",
"ai.prefs.open_at_start_note": "מציג את חלונית העוזר (או את החלון הנפרד שלה) בכל הפעלה של SolidCraft. לא כש-SolidCraft מופעל עם --control.",
"ai.prefs.override_all_warning": "הסוכן הזה עשוי לשמור, לדרוס, לייצא או להחליף את התכן בלי לשאול.",
"ai.prefs.permission_all": "התרת הכול",
"ai.prefs.permission_all_summary": "כלי SolidCraft הזמינים פועלים בלי לשאול.",
@@ -679,6 +691,9 @@
"cmd.appearance_assign": "מראה",
"cmd.appearance_library": "ספריית מראים",
"cmd.appearance_list": "רשימת מראים",
"cmd.appearance_paint": "צביעת מראה",
"cmd.appearance_pick_up": "בחירת מראה",
"cmd.appearance_replace": "החלפת מראה",
"cmd.assembly_bom": "רשימת חלקים",
"cmd.body_rename": "שינוי שם גוף",
"cmd.browser_collapse_all": "כיווץ הכול",
@@ -766,6 +781,7 @@
"cmd.document_revision": "גרסת התכן",
"cmd.document_units": "שינוי יחידות התכן",
"cmd.edit_checkpoint": "נקודת שמירה",
"cmd.edit_repeat": "חזרה על הפקודה האחרונה",
"cmd.edit_restore_checkpoint": "שחזור נקודת שמירה",
"cmd.engine_commands": "רשימת פקודות",
"cmd.engine_describe": "תיאור פקודה",
@@ -879,9 +895,11 @@
"cmd.plastic_snap_fit": "חיבור הצמדה",
"cmd.render_aging_describe": "הזדקנות משטח בשכבות",
"cmd.render_backends": "מנועי הדמיה",
"cmd.render_batch": "רינדור כל הצילומים",
"cmd.render_camera_fit": "מסגור נושא ההדמיה",
"cmd.render_camera_get": "בדיקת מצלמת ההדמיה",
"cmd.render_cancel": "ביטול הדמיה",
"cmd.render_compare": "השוואת תמונות",
"cmd.render_effects_describe": "אפקטי עדשה ואפקטים יצירתיים",
"cmd.render_export": "ייצוא תמונת הדמיה",
"cmd.render_hdr_inspect": "בדיקת מקור HDR",
@@ -897,6 +915,7 @@
"cmd.render_library_surface_prepare": "הכנת מפות משטח מעוצבות",
"cmd.render_library_surface_unbind": "הסרת קישור משטח",
"cmd.render_library_targets": "חיפוש יעדים לספריית ההדמיה",
"cmd.render_lights_solo": "בידוד אור",
"cmd.render_optics_bind": "שמירת קישור אופטי",
"cmd.render_optics_unbind": "הסרת קישור אופטי",
"cmd.render_preflight": "בדיקת בקשת הדמיה",
@@ -1006,7 +1025,9 @@
"cmd.sketch_spline_control_point": "ספליין נקודות בקרה",
"cmd.sketch_spline_control_point_5": "ספליין נקודות בקרה (מעלה 5)",
"cmd.sketch_spline_fit_point": "ספליין נקודות התאמה",
"cmd.sketch_spline_insert_point": "הוספת נקודה לספליין",
"cmd.sketch_spun_profile": "פרופיל סיבובי",
"cmd.sketch_toggle_diameter": "מעבר בין רדיוס לקוטר",
"cmd.sketch_toggle_driven": "מעבר בין מידה מונעת למניעה",
"cmd.sketch_transform": "התמרה",
"cmd.sketch_trim_path": "גיזום לאורך מסלול",
@@ -1072,6 +1093,7 @@
"cmd.view_rename": "שינוי שם תצוגה שמורה",
"cmd.view_right": "תצוגה ימנית",
"cmd.view_save": "תצוגה שמורה חדשה",
"cmd.view_shots_strip": "סרגל צילומים",
"cmd.view_spin": "סיבוב מבט רציף",
"cmd.view_tool": "כלי ניווט",
"cmd.view_top": "תצוגה עליונה",
@@ -1145,6 +1167,7 @@
"document.save_changes": "לשמור את השינויים ב־{name}?",
"document.save_copy": "שמירת עותק…",
"document.unsaved_changes": "ב־{name} יש שינויים שלא נשמרו. ללא שמירה, הם יאבדו.",
"drive.rigid": "{joint} הוא מפרק קשיח: הוא מחזיק את החלקים שלו במקומם, ולכן אין מה להניע. כדי שהחלקים יזוזו, שנו את הסוג שלו ב״עריכת מפרק״: Revolute לסיבוב, Slider להחלקה, Cylindrical לשניהם.",
"drive.stopped_by_contact": "נעצר ב־{value}: {a} היה עובר דרך {b}",
"drive.stopped_by_loop": "נעצר ב־{value}: הלולאה דרך {joint} לא יכולה להיסגר יותר",
"drive.unconnected": "שום דבר לא זז: {part} אינו מחובר לשום דבר מעוגן",
@@ -1286,6 +1309,9 @@
"install.steps_intro": "Safari מתקין יישומי רשת מהתפריטים שלו:",
"install.title": "התקנת SolidCraft",
"issue.body_label": "טקסט הדוח",
"issue.note_changed_while_waiting": "ההגדרות או המודל השתנו בזמן ההמתנה לתצוגה המקדימה. צלם את המסך מחדש כדי לעדכן את הפרטים הטכניים לתצוגה הנוכחית.",
"issue.note_details_kept": "צילום המסך נלכד מקומית. הפרטים הטכניים שערכת נשמרו: בדוק שהם עדיין תואמים לתצוגה הנוכחית.",
"issue.note_details_updated": "הפרטים הטכניים עודכנו כך שיתאימו לתצוגה הנוכחית. צילום המסך נלכד מקומית. בדוק אותו לפני שתצרף אותו ב־GitHub.",
"issue.note_image_copied": "יש להדביק את התמונה בטיוטה ב־GitHub, או לשמור את קובץ ה־PNG אם העתקת תמונות אינה זמינה.",
"issue.note_opened": "יש לבדוק את הטיוטה ב־GitHub ולצרף את צילום המסך. הלחצן Submit new issue ב־GitHub מפרסם אותה.",
"issue.note_opened_copied": "הדוח הועתק. יש להדביק אותו בטיוטה ב־GitHub, לצרף את צילום המסך ואז ללחוץ על Submit new issue ב־GitHub.",
@@ -1352,6 +1378,8 @@
"material.editor.unknown_card": "התכונות ההנדסיות אינן ידועות. ייתכן שהשמירה תשמור רק את הצפיפות המוקצית ואת המראה הקיים.",
"material.editor.user_supplied": "סופק על ידי המשתמש",
"material.editor.wood": "עץ",
"material.selection.choose": "בחירת חומר",
"material.selection.mixed": "חומרים שונים",
"measure.at": "מיקום",
"measure.item": "פריט {number}",
"messages.ago_h": "לפני {n} שע'",
@@ -1381,6 +1409,8 @@
"nav.free_orbit.help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. הצמדת הצירים עם Shift מושבתת.",
"nav.free_orbit.label": "סיבוב חופשי",
"nav.free_orbit.snap_help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. יש להחזיק את Shift בזמן גרירת סיבוב כדי להצמיד את כיוון התצוגה ואת הגלגול.",
"nav.free_orbit_upright.help": "גרירת העכבר הצידה מסובבת את המודל סביב הציר האנכי של העולם; גרירה למעלה ולמטה מטה אותו מעבר לקודקוד ללא הגבלה.",
"nav.free_orbit_upright.label": "סיבוב סביב הציר האנכי",
"nav.fusion.help": "גרירה אמצעית מזיזה, Shift וגרירה אמצעית מסובבים, גרירה ימנית מסובבת",
"nav.inventor.help": "גרירה אמצעית מזיזה, Shift וגרירה אמצעית מסובבים, Ctrl וגרירה אמצעית מבצעים זום, גרירה ימנית מסובבת",
"nav.openscad.help": "גרירה שמאלית על שטח ריק מסובבת, Shift וגרירה שמאלית בוחרים במלבן, גרירת גאומטריית סקיצה עורכת אותה; גרירה ימנית או אמצעית מזיזה, Shift וגרירה ימנית או אמצעית מבצעים זום",
@@ -1456,6 +1486,10 @@
"prefs.design_note": "ההגדרות שתכנים חדשים מתחילים איתן. תכנים פתוחים שומרים על ההגדרות שלהם.",
"prefs.devices": "עכבר תלת־ממדי",
"prefs.dialog_buttons": "לחצני תיבות דו־שיח",
"prefs.dialogs": "תיבות דו־שיח",
"prefs.dialogs_edge": "בקצה התצוגה",
"prefs.dialogs_left_column": "בעמודה השמאלית",
"prefs.dialogs_note": "היכן נפתחות תיבות הדו־שיח של הפקודות: מעוגנות בקצה התצוגה, או בעמודה השמאלית במקום הדפדפן וההיסטוריה, שחוזרים כפי שהיו כשהתיבה נסגרת.",
"prefs.display": "תצוגה",
"prefs.edges": "מקצועות והצללה",
"prefs.extend": "הרחבה",
@@ -1515,6 +1549,8 @@
"prefs.opaque": "אטום",
"prefs.orbit_snap": "החזקת Shift בזמן סיבוב מצמידה לציר.",
"prefs.orientation": "כיוון",
"prefs.origin_plane_names": "שמות מישורי הראשית",
"prefs.origin_plane_names_note": "השמות המוצגים בדפדפן, בתיבות הדו־שיח וברשימות. {top} הוא XY, {front} הוא XZ ו־{right} הוא YZ, כש־Z למעלה. פקודות, סקריפטים ותכנונים שמורים שומרים על XY, XZ ו־YZ.",
"prefs.overall_speed": "מהירות כללית",
"prefs.placement": "מיקום",
"prefs.prefer_free_sketch_points": "העדפת נקודות סקיצה ניתנות להזזה כשנקודות חופפות",
@@ -1555,6 +1591,12 @@
"prefs.supersampling_brief": "מצייר את המבט גדול יותר ומקטין אותו: חד מאוד, וכבד יותר.",
"prefs.tab_accent_note": "צבע ההדגשה עוקב אחר הלשונית הפעילה",
"prefs.theme_file": "קובץ ערכת נושא",
"prefs.theme_follow": "מעקב אחר קובץ ערכת נושא",
"prefs.theme_follow_error": "לא הוחל: {error}. ערכת הנושא התקינה האחרונה נשארת פעילה.",
"prefs.theme_follow_launch": "מעקב להפעלה זו בלבד (--theme-file); נטען מחדש בכל פעם שכלי אחר משכתב את הקובץ.",
"prefs.theme_follow_note": "נטען מחדש בכל פעם שכלי אחר משכתב את הקובץ. בחירת ערכת נושא מפסיקה את המעקב.",
"prefs.theme_follow_off": "כבוי",
"prefs.theme_follow_stop": "הפסקת המעקב",
"prefs.timeline_half_hour": "−30 דק׳",
"prefs.timeline_now": "עכשיו",
"prefs.timeline_quarter_hour": "−15",
@@ -1619,6 +1661,7 @@
"render.aging_target_search": "סינון שמות גופים",
"render.aging_upward": "שקיעה על משטחים הפונים מעלה",
"render.all_controls_help": "בדיקה צפופה של כל הפקדים הנוכחיים. דפי המקטעים מאפשרים עריכה ממוקדת בלי לשנות את הבקשה עצמה.",
"render.all_shots_label": "כל הצילומים",
"render.appearance_physics_separate": "המראה החזותי אינו מקצה צפיפות, נתונים תרמיים או תכונות הנדסיות אחרות.",
"render.availability_native": "קומפוזיציה מקורית; יש לסקור לפני ההחלה",
"render.availability_projection": "השלכה נתמכת; ראו היקף והשמטות",
@@ -1773,10 +1816,13 @@
"render.effects_reset_lens": "איפוס אפקטי העדשה",
"render.effects_seed": "זרע הדוגמה",
"render.export_display": "ייצוא: PNG לתצוגה",
"render.export_image_label": "ייצוא תמונה",
"render.export_linear": "ייצוא: EXR ליניארי גולמי",
"render.exr_pending": "GPU יכול לשמור EXR ליניארי גולמי. דחיסה, תמונות רב־שכבתיות ומעברי הדמיה נלווים עדיין אינם זמינים.",
"render.film_grain": "גרעיניות צילומית",
"render.fit": "התאמת המוצר לתצוגה",
"render.fixed_camera": "מצלמה",
"render.fixed_world": "עולם",
"render.flare_ghosts": "טבעות רפאים לכל מקור",
"render.flare_halo": "הילת בוהק",
"render.flare_streak": "פס בסגנון אנמורפי",
@@ -1803,6 +1849,12 @@
"render.hdr_primaries_srgb": "צבעי יסוד: sRGB/D65",
"render.hdr_primaries_undeclared": "המקור אינו מצהיר על צבעי יסוד; יש לעיין בדרישות הצבע של מנוע ההדמיה.",
"render.hdr_resource_missing": "בתכן הזה חסר משאב ה־HDR השמור.",
"render.heading_batch": "אצווה",
"render.heading_frame": "מסגרת",
"render.heading_lens": "עדשה",
"render.heading_results": "תוצאות",
"render.heading_shots": "צילומים",
"render.hide_shots_label": "הסתר את סרגל הצילומים",
"render.image_advanced": "פקדי GPU מתקדמים",
"render.image_cancelled": "ההדמיה בוטלה",
"render.image_cancelling": "מבטל",
@@ -1834,6 +1886,7 @@
"render.image_width": "רוחב",
"render.include_rig_cards": "הכללת כרטיסים ודגלים סבילים",
"render.inspect_scene": "רענון סיכום הסצנה שנלכדה",
"render.inspector": "מפקח",
"render.keep_editing": "המשך עריכה",
"render.layer_down": "הזזה למטה",
"render.layer_enabled": "השכבה מופעלת",
@@ -1902,6 +1955,8 @@
"render.library_unfavorite": "הסרה מהמועדפים",
"render.library_uniform_ack": "שימוש בקירוב האחיד ללא אפקטים מרחביים או מתקדמים",
"render.library_usable": "השלכות ניתנות להרצה",
"render.light_anchor_pending": "שינוי מערכת הצירים של האור אינו נתמך עדיין.",
"render.lights_fixed_to": "האורות מקובעים אל",
"render.linear_help": "שומר מאסטר RGBA בנקודה צפה של 32 סיביות לפני הסרת רעש, חשיפה, מיפוי גוונים, מסננים ורקע. sRGB/D65 ליניארי; מוכפל מראש בכיסוי. זיכרון נוסף מאושר במפורש. ללא דחיסה או מעברים נוספים.",
"render.linear_retain": "שמירת EXR ליניארי גולמי",
"render.linear_unavailable": "לפריים הגמור שנבחר אין EXR שמור. יש להפעיל שמירה לפני ההדמיה; PNG ישן אינו יכול לשחזר קרינה גולמית.",
@@ -1987,6 +2042,7 @@
"render.output_over_budget": "התמונה המבוקשת חורגת ממעטפת המשאבים שנבחרה. יש לבחור במפורש גודל או פרופיל מתאימים; אין הורדה אוטומטית.",
"render.output_ratio_refused": "הממד המקושר חורג ממגבלת הגודל של השיטה הזאת. הגודל הקודם נשמר; יש לבטל את נעילת היחס או לבחור ערך קטן יותר.",
"render.output_swap_orientation": "החלפת רוחב וגובה",
"render.output_tools_inspector": "ערוך גודל ופורמט במפקח הפלט.",
"render.page_aging": "הזדקנות וזיהום",
"render.page_all": "כל הפקדים",
"render.page_appearance": "מראה",
@@ -1998,6 +2054,7 @@
"render.page_lighting": "תאורה",
"render.page_output": "פלט",
"render.page_scene": "סצנה",
"render.paint_label": "צביעה",
"render.passive_stage": "קרקע ובמה סבילה",
"render.passive_stage_help": "לוחות סבילים סופיים שייכים לתמונות מודמות בלבד. הם אינם מוסיפים גופי CAD או מסה. המיקום קבוע אחרי ההחלה; חיתוך עם גבולות המוצר נדחה. CPU אינו מספק תאורת החזרה מפוזרת.",
"render.perspective": "מצלמה פרספקטיבית",
@@ -2013,6 +2070,10 @@
"render.physical_prepare_plan": "הכנת תוכנית יצירה (ללא החלה)",
"render.physical_separate": "מראה אינו זיהוי חומר. אלה ייחוסים מתועדי מקור, לא תכונות אפקטיביות מאושרות. המסה, החומרים ההנדסיים והאופטיקה הקיימים אינם משתנים.",
"render.physical_unknown": "לא נקבע מועמד מספרי. יש לציין דרגה, הרכב או מבנה; לא ידוע אינו אפס.",
"render.pick_component": "רכיב",
"render.pick_label": "בחירה",
"render.pick_up_label": "בחירת מראה",
"render.pick_waiting": "בחירת מוצר אינה נתמכת עדיין.",
"render.post_contrast": "ניגודיות",
"render.post_gain_b": "הגבר כחול",
"render.post_gain_g": "הגבר ירוק",
@@ -2025,6 +2086,7 @@
"render.preview": "תצוגה מקדימה מובנית",
"render.preview_help": "הצללת רסטר ושקיפות ממוזגת. שבירת אור, HDRI והדמיה סופית עדיין אינם זמינים.",
"render.preview_image_settings_help": "התצוגה המקדימה החיה שומרת על תאורת סטודיו קבועה. החשיפה ותאורת התמונה חלות על משימת התמונה בעת בדיקה או התחלה.",
"render.preview_lighting_help": "התצוגה המקדימה החיה מציגה אור ישיר בלבד וללא צללים.",
"render.preview_retry": "ניסיון חוזר של התצוגה המקדימה החיה",
"render.preview_unavailable_help": "התצוגה המקדימה החיה אינה זמינה. ההגדרות השמורות ופקדי משימת התמונה נשארים זמינים. יש לשנות את הסצנה או לנסות שוב במפורש; סצנה שנדחתה אינה נבנית מחדש בכל פריים.",
"render.preview_upload": "מטען הקודקודים של התצוגה המקדימה החיה",
@@ -2038,8 +2100,10 @@
"render.radial_distortion": "עיוות רדיאלי",
"render.regrade_action": "תיקון צבע מחדש למאסטר הגולמי (ללא הסרת רעש)",
"render.regrade_help": "יוצר PNG נפרד מהמאסטר הגולמי הגמור שנבחר, באמצעות פקדי התצוגה האלה. אינו עוקב אחר קרניים, אינו משנה את הגודל, המצלמה או האורות שנלכדו ואינו דורס את ה־EXR. הסרת רעש אינה מוחלת שוב: מאסטרים גולמיים אינם שומרים את המדריכים שלה. התוצר הנגזר נשאר ניתן לייצוא באותם פקדי משימה.",
"render.regrade_label": "כיול מחדש",
"render.reload_settings": "טעינה מחדש של ההגדרות השמורות",
"render.replace_ground": "אישור החלפת הבמה הקיימת",
"render.replace_label": "החלפה",
"render.replace_saved_surface": "החלפת קישור שמור קיים ליעד הזה",
"render.restore_camera": "שחזור המצלמה השמורה",
"render.result_actual_help": "פיקסל תמונה אחד לכל פיקסל תצוגה.",
@@ -2085,6 +2149,7 @@
"render.sampling_title": "דגימה אדפטיבית ודגימת העברה",
"render.save_continue": "שמירה והמשך",
"render.save_setup": "שמירת ההגדרה",
"render.save_shot": "שמור צילום",
"render.saved_maps_runtime_conflict": "אי אפשר לשלב קישורי משטח שמורים עם דריסות אופטיות זמניות או עם HDR שלא נשמר. יש לשמור את ה־HDR או להסיר את הקלטים הזמניים במפורש.",
"render.saved_surface_remove": "הסרת הקישור השמור",
"render.saved_surfaces": "קישורי משטח שמורים",
@@ -2104,6 +2169,9 @@
"render.setup_runtime_unsupported": "עדיין אי אפשר לשמור את המשימה המלאה הזאת: יש בה מפות או קישורים לגופי סצנה. יש להסיר את הקלטים הזמניים האלה לפני שמירת הגדרה ניידת.",
"render.setup_save_missing": "תצורת ההדמיה השמורה לא נמצאה אחרי שהפקודה הסתיימה.",
"render.sharpening": "חידוד פרטים",
"render.shell_tab_appearance": "מראה רינדור",
"render.shell_tab_camera": "מצלמת רינדור",
"render.shell_tab_scene": "סצנת רינדור",
"render.shot_duplicate": "שכפול הצילום השמור",
"render.shot_manager": "צילומים שמורים",
"render.shot_new_name": "שם הצילום החדש",
@@ -2112,11 +2180,19 @@
"render.shot_remove_title": "להסיר את הצילום השמור?",
"render.shot_rename": "שינוי שם הצילום השמור",
"render.shot_saved_only": "יש לשמור קודם את הטיוטה הנוכחית. שכפול ושינוי שם שומרים על האפשרויות השמורות ועל הפניות המשאבים במדויק.",
"render.show_shots_label": "הצג את סרגל הצילומים",
"render.sky_ground": "קרקע · RGB ליניארי",
"render.sky_horizon": "אופק · RGB ליניארי",
"render.sky_zenith": "זנית · RGB ליניארי",
"render.soft_focus": "מיקוד רך",
"render.stage_frame": "מסגרת",
"render.stage_light": "אור",
"render.stage_look": "מראה",
"render.stage_orthographic": "אורתוגרפי",
"render.stage_output": "פלט",
"render.stage_panel_count": "לוחות שמורים",
"render.stage_preview_note": "תצוגה מקדימה משולבת · אור ישיר",
"render.strip_label": "סרגל",
"render.studio_add_area": "הוספת אור שטח",
"render.studio_ambient": "קרינת סביבה קבועה",
"render.studio_ambient_rgb": "קרינת סביבה קבועה (RGB ליניארי)",
@@ -2173,6 +2249,12 @@
"render.volume_max": "גבולות מרביים · mm בעולם",
"render.volume_min": "גבולות מזעריים · mm בעולם",
"render.volume_title": "נפח הומוגני תחום",
"render.wait_batch": "לא נתמך עדיין: תור רינדור של צילומים שמורים",
"render.wait_compare": "לא נתמך עדיין: השוואת תמונות שנשמרו",
"render.wait_paint": "לא נתמך עדיין: בחירת מוצר וצביעת מראה",
"render.wait_pick_up": "לא נתמך עדיין: בחירת מוצר ודגימת מראה",
"render.wait_replace": "לא נתמך עדיין: החלפת כל השימושים במראה",
"render.wait_solo": "לא נתמך עדיין: נראות אורות ובידוד",
"sample.bracket.about": "אוגן בסיס עם אוגני קצה וקיפול חוזר, לפי כלל של 2 mm",
"sample.bracket.name": "תושבת פח",
"sample.enclosure.about": "קופסה עם קליפה, שפה, בליטות לברגים, משען ותפס הצמדה",
@@ -2264,6 +2346,7 @@
"timeline.delete_after_marker_one": "מחיקת פיצ'ר אחד אחרי סמן ציר הזמן",
"timeline.delete_after_marker_other": "מחיקת {count} פיצ'רים אחרי סמן ציר הזמן",
"timeline.delete_folders_tip": "שמירת הפיצ'רים והתיקיות המקוננות שלהן בציר הזמן",
"timeline.delete_folders_with_features_tip": "מחיקת התיקיות עם כל הפיצ'רים שבהן והתיקיות המקוננות בהן",
"timeline.delete_many": "מחיקת {count} הפיצ'רים שנבחרו",
"timeline.feature_error": "שגיאה: {message}",
"timeline.feature_hint": "לחיצה כפולה לעריכה · גרירה לשינוי הסדר · לחיצה ימנית לעוד",
@@ -2273,6 +2356,7 @@
"timeline.move_to_group": "העברת פיצ'רים לתיקייה",
"timeline.rename_group": "שינוי שם תיקיית פיצ'רים",
"timeline.reorder_many": "שינוי סדר פיצ'רים",
"timeline.suppress_folders_tip": "השבתה או ביטול השבתה של כל הפיצ'רים בתיקיות ובתיקיות המקוננות בהן",
"timeline.ungroup": "הסרת תיקיות פיצ'רים",
"to the face": "עד הפאה",
"to the next face": "עד הפאה הבאה",
@@ -2283,7 +2367,11 @@
"toolbar.render_camera": "מצלמה",
"toolbar.render_environment": "סביבה",
"toolbar.render_lighting": "תאורה",
"toolbar.render_lights": "אורות",
"toolbar.render_result": "תוצאה",
"toolbar.render_scene": "סצנה",
"toolbar.render_stage": "שלב",
"toolbar.render_studio": "סטודיו",
"trace.points": "{count} נקודות לאורך המסלול",
"trace.stopped_by_contact": "מסתיים בדגימה {sample}: {a} היה עובר דרך {b}",
"trace.stopped_by_loop": "מסתיים בדגימה {sample}: הלולאה דרך {joint} לא יכולה להיסגר יותר",
@@ -2462,6 +2550,7 @@
"ui.agent_chat_import_failed": "{name}: הייבוא נכשל: {error}",
"ui.agent_chat_intro": "צ'אט עם סוכן קוד שכבר נמצא בשימוש, או עם נקודת קצה של מודל לבחירתך. הסוכן עובד על התכן הפתוח דרך הכלים של SolidCraft: השינויים מוצגים תוך כדי ביצוע, וכל הודעה מתבטלת כצעד אחד. מה מותר לו לעשות בלי לשאול נקבע בהעדפות > AI.",
"ui.agent_chat_key_in_env": "מפתח מוגדר",
"ui.agent_chat_last_missing": "הסוכן שבו השתמשת לאחרונה ({agent}) כבר אינו זמין. בחר סוכן ולחץ על „ניסיון חוזר”.",
"ui.agent_chat_left": "שמאל",
"ui.agent_chat_limit": "הסוכן הגיע למגבלה של {limit} שינויים להודעה אחת. יש לשלוח הודעה נוספת כדי שימשיך.",
"ui.agent_chat_login_found": "נמצאה התחברות",
@@ -2508,6 +2597,7 @@
"ui.agent_chat_remove": "הסרה מהתור",
"ui.agent_chat_remove_file": "הסרה מההודעה הזו",
"ui.agent_chat_remove_imported": "הסרה מההודעה הזו; החלק נשאר בתכן (ביטול מסיר אותו)",
"ui.agent_chat_retry": "ניסיון חוזר",
"ui.agent_chat_right": "ימין",
"ui.agent_chat_save": "שמירה",
"ui.agent_chat_send": "שליחה",
@@ -2520,6 +2610,7 @@
"ui.agent_chat_state_auth": "נדרשת התחברות",
"ui.agent_chat_state_busy": "עובד…",
"ui.agent_chat_state_exited": "הסוכן נעצר",
"ui.agent_chat_state_failed": "ההתחברות נכשלה",
"ui.agent_chat_state_off": "לא מחובר",
"ui.agent_chat_state_ready": "מוכן",
"ui.agent_chat_state_starting": "מופעל…",
@@ -2823,6 +2914,7 @@
"ui.fully_constrained": "מאולצת במלואה",
"ui.function": "פונקציה",
"ui.general": "כללי",
"ui.gpu_memory_lowered": "הזיכרון של כרטיס המסך אזל: התצוגה מוצגת באיכות נמוכה יותר.",
"ui.graphics": "גרפיקה",
"ui.graphics_custom": "מותאם אישית",
"ui.graphics_performance": "ביצועים",
@@ -3093,6 +3185,7 @@
"ui.render": "הדמיה",
"ui.renderer": "מנוע גרפי",
"ui.repeat": "חזרה",
"ui.repeat_nothing": "אין עדיין מה לחזור עליו: התחל קודם כלי או תיבת דו־שיח.",
"ui.replace_with_link_done": "העותקים הם עכשיו מיקומים של התכן המקושר.",
"ui.report_a_bug": "דיווח על באג",
"ui.reset": "איפוס",
@@ -3315,5 +3408,6 @@
"web.open_link": "לפתוח את התכן המקושר?",
"web.open_link_confirm": "יש שינויים שלא נשמרו. לפתוח את התכן המקושר בלשונית חדשה?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ בחירה · Enter / Tab השלמה · Esc סגירה",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ בחירה · Enter מחיל את מה שהוקלד · Esc סגירה"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ בחירה · Enter מחיל את מה שהוקלד · Esc סגירה",
"⌘Enter continues on GitHub.": "⌘Enter ממשיך ב־GitHub."
}
+6
View File
@@ -1002,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}",
+6
View File
@@ -1002,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}",
+8
View File
@@ -980,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",
@@ -1038,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}",
+96 -2
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "中間半径を追加",
"Add Row": "行を追加",
"Add a tool": "ツールを追加",
"Add all bodies": "すべてのボディを追加",
"Add bend": "曲げを追加",
"Add condition": "条件を追加",
"Add or edit rule": "ルールを追加または編集",
"Add to New Folder": "新しいフォルダーに追加",
"Add visible bodies": "表示中のボディを追加",
"Advanced": "詳細",
"Against": "相手",
"Alignment": "配置",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "境界サーフェス",
"BROWSER": "ブラウザ",
"Bar": "平鋼",
"Base Center": "底面の中心",
"Bend": "曲げ角度",
"Bend Edges": "曲げエッジ",
"Bend Line Position": "曲げ線位置",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
"Create Issue…": "Issueを作成…",
"Cross curves": "交差する曲線",
"Ctrl+Enter continues on GitHub.": "Ctrl+EnterでGitHubに進みます。",
"Cube": "キューブ",
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
"Current perspective view": "現在の透視投影",
@@ -152,6 +156,7 @@
"Customize…": "カスタマイズ…",
"Cylindrical face": "円筒面",
"Delete": "削除",
"Delete Folders and Features…": "フォルダーとフィーチャーを削除…",
"Delete Selected Features…": "選択したフィーチャーを削除…",
"Delete Selected Folders": "選択したフォルダーを削除",
"Delete all": "すべて削除",
@@ -339,7 +344,7 @@
"Name on import": "インポート名",
"Nearest": "最も近い",
"New": "新規",
"New Comp.": "新規コンポーネント",
"New Comp.": "新規\nコンポ",
"New Sketch": "新規スケッチ",
"New rule…": "新規ルール…",
"New study": "新規スタディ",
@@ -430,6 +435,7 @@
"Remove this point": "この点を削除",
"Rename Feature": "フィーチャーの名前を変更",
"Rename Folder": "フォルダーの名前を変更",
"Render Stage": "レンダリング段階",
"Reopen designs": "デザインを再度開く",
"Reopen the designs that were open when SolidCraft was last closed": "前回 SolidCraft を閉じたときに開いていたデザインを再度開く",
"Replace existing library file": "既存のライブラリファイルを置き換える",
@@ -534,6 +540,7 @@
"Style": "スタイル",
"Summary": "概要",
"Suppress": "抑制",
"Suppress Selected Folders": "選択したフォルダーを抑制",
"Suppressed": "抑制",
"Surface": "サーフェス",
"Surface, or faces to turn over": "サーフェス、または裏返す面",
@@ -581,6 +588,7 @@
"UNTRIM": "トリム解除",
"Unit": "単位",
"Unsuppress": "抑制を解除",
"Unsuppress Selected Folders": "選択したフォルダーの抑制を解除",
"Updating the preview": "プレビューを更新中",
"Use active author placement": "アクティブな作成元配置を使用",
"Use fixed plane": "固定平面を使用",
@@ -628,6 +636,8 @@
"ai.prefs.background_commands": "エージェントのコマンドをバックグラウンドで実行",
"ai.prefs.connect_apps": "このコンピューターの AI アプリで SolidCraft を使えるようにする",
"ai.prefs.connect_apps_note": "SolidCraft の起動時に、このコンピューターで見つかった AI アプリ(OpenCode、Claude Desktop、Claude Code、Cursor、Codex、Gemini CLI、Copilot CLI、Windsurf、Zed、VS Code)へ MCP サーバーとして追加します。追加または更新するのは solidcraft のエントリのみで、変更済みのエントリには触れず、ファイルは先にバックアップされます。オフにすると追加したエントリを削除します。",
"ai.prefs.connect_at_start": "SolidCraft の起動時に前回使用したエージェントに接続",
"ai.prefs.connect_at_start_note": "SolidCraft を起動するたびに、前回接続したエージェント (またはデフォルトのエージェント) をバックグラウンドで起動し、すぐにチャットできるようにします。--control を付けて起動した場合は行いません。接続できない場合、アシスタントに「接続に失敗しました」と「再試行」ボタンが表示されます。",
"ai.prefs.follow_camera": "カメラをスケッチに向ける",
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
@@ -635,6 +645,8 @@
"ai.prefs.make_default": "既定にする",
"ai.prefs.no_agents": "エージェントはまだありません。追加するか、更新してインストール済みのエージェントを検索してください。",
"ai.prefs.no_saved_approvals": "保存された許可はありません。",
"ai.prefs.open_at_start": "SolidCraft の起動時にアシスタントパネルを開く",
"ai.prefs.open_at_start_note": "SolidCraft を起動するたびにアシスタントパネル (または独立したウィンドウ) を表示します。--control を付けて起動した場合は行いません。",
"ai.prefs.override_all_warning": "このエージェントは確認せずに設計を保存、上書き、書き出し、置換できます。",
"ai.prefs.permission_all": "すべて許可",
"ai.prefs.permission_all_summary": "利用可能なSolidCraftツールを確認せず実行します。",
@@ -679,6 +691,9 @@
"cmd.appearance_assign": "外観",
"cmd.appearance_library": "外観ライブラリ",
"cmd.appearance_list": "外観を一覧表示",
"cmd.appearance_paint": "外観をペイント",
"cmd.appearance_pick_up": "外観を取得",
"cmd.appearance_replace": "外観を置換",
"cmd.assembly_bom": "部品表",
"cmd.body_rename": "ボディの名前を変更",
"cmd.browser_collapse_all": "すべて折りたたむ",
@@ -766,6 +781,7 @@
"cmd.document_revision": "デザイン リビジョン",
"cmd.document_units": "デザインの単位を変更",
"cmd.edit_checkpoint": "チェックポイント",
"cmd.edit_repeat": "最後のコマンドを繰り返す",
"cmd.edit_restore_checkpoint": "チェックポイントを復元",
"cmd.engine_commands": "コマンドを一覧表示",
"cmd.engine_describe": "コマンドの詳細を表示",
@@ -879,9 +895,11 @@
"cmd.plastic_snap_fit": "スナップ フィット",
"cmd.render_aging_describe": "レイヤー式表面エイジング",
"cmd.render_backends": "レンダリング バックエンド",
"cmd.render_batch": "すべてのショットをレンダリング",
"cmd.render_camera_fit": "レンダー対象をフレームに合わせる",
"cmd.render_camera_get": "レンダーカメラを確認",
"cmd.render_cancel": "レンダリングをキャンセル",
"cmd.render_compare": "画像を比較",
"cmd.render_effects_describe": "レンズとクリエイティブ効果",
"cmd.render_export": "PNGをエクスポート",
"cmd.render_hdr_inspect": "HDRソースを調査",
@@ -897,6 +915,7 @@
"cmd.render_library_surface_prepare": "作成済みサーフェスマップを準備",
"cmd.render_library_surface_unbind": "Remove Surface Binding",
"cmd.render_library_targets": "レンダリングリソースの対象を検索",
"cmd.render_lights_solo": "ライトを単独表示",
"cmd.render_optics_bind": "光学割り当てを保存",
"cmd.render_optics_unbind": "光学割り当てを削除",
"cmd.render_preflight": "レンダリング要求を確認",
@@ -1006,7 +1025,9 @@
"cmd.sketch_spline_control_point": "制御点スプライン",
"cmd.sketch_spline_control_point_5": "制御点スプライン (次数 5)",
"cmd.sketch_spline_fit_point": "通過点スプライン",
"cmd.sketch_spline_insert_point": "スプライン点を追加",
"cmd.sketch_spun_profile": "回転プロファイル",
"cmd.sketch_toggle_diameter": "半径 / 直径の切り替え",
"cmd.sketch_toggle_driven": "従動寸法の切り替え",
"cmd.sketch_transform": "変形",
"cmd.sketch_trim_path": "パスに沿ってトリム",
@@ -1072,6 +1093,7 @@
"cmd.view_rename": "名前付きビューの名前を変更",
"cmd.view_right": "右側面ビュー",
"cmd.view_save": "新規名前付きビュー",
"cmd.view_shots_strip": "ショットストリップ",
"cmd.view_spin": "連続オービット",
"cmd.view_tool": "ナビゲーションツール",
"cmd.view_top": "上面ビュー",
@@ -1145,6 +1167,7 @@
"document.save_changes": "{name} への変更を保存しますか?",
"document.save_copy": "コピーを保存…",
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
"drive.rigid": "{joint} は剛体ジョイントです。部品をその場に固定するため、駆動するものがありません。動かすには「ジョイントを編集」で種類を変更してください: 回転させるなら Revolute、スライドさせるなら Slider、両方なら Cylindrical。",
"drive.stopped_by_contact": "{value} で停止: {a} が {b} を通り抜けてしまいます",
"drive.stopped_by_loop": "{value} で停止: {joint} を通るループはこれ以上閉じられません",
"drive.unconnected": "何も動きません: {part} は空間固定されたものに接続されていません",
@@ -1286,6 +1309,9 @@
"install.steps_intro": "Safari では、Web アプリを Safari 自身のメニューからインストールします:",
"install.title": "SolidCraft をインストール",
"issue.body_label": "レポート本文",
"issue.note_changed_while_waiting": "プレビューを待っている間に設定またはモデルが変更されました。スクリーンショットを撮り直すと、技術的な詳細が現在の表示に合わせて更新されます。",
"issue.note_details_kept": "スクリーンショットをローカルに取得しました。編集した技術的な詳細はそのまま残しました。現在の表示と合っているか確認してください。",
"issue.note_details_updated": "技術的な詳細を現在の表示に合わせて更新しました。スクリーンショットをローカルに取得しました。GitHub に添付する前に確認してください。",
"issue.note_image_copied": "GitHubの下書きに画像を貼り付けてください。画像をコピーできない場合はPNGを保存してください。",
"issue.note_opened": "GitHubの下書きを確認し、スクリーンショットを添付してください。GitHubのSubmit new issueボタンで公開されます。",
"issue.note_opened_copied": "レポートをコピーしました。GitHubの下書きに貼り付け、スクリーンショットを添付してから、GitHubでSubmit new issueをクリックしてください。",
@@ -1352,6 +1378,8 @@
"material.editor.unknown_card": "工学特性は不明です。割り当て密度と既存の外観だけでも保存できます。",
"material.editor.user_supplied": "ユーザー提供",
"material.editor.wood": "木材",
"material.selection.choose": "材料を選択",
"material.selection.mixed": "異なる材料",
"measure.at": "位置",
"measure.item": "項目 {number}",
"messages.ago_h": "{n} 時間前",
@@ -1381,6 +1409,8 @@
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
"nav.free_orbit.label": "自由回転",
"nav.free_orbit.snap_help": "マウスと3Dマウスで両極を越えて回転でき、ロールも保持されます。オービットのドラッグ中にShiftを押すと、視線方向とロールがスナップします。",
"nav.free_orbit_upright.help": "マウスを左右にドラッグするとワールドの垂直軸を中心にモデルが回転し、上下にドラッグすると制限なく真上を越えて傾きます。",
"nav.free_orbit_upright.label": "垂直軸を中心に回転",
"nav.fusion.help": "中ドラッグでパン、Shift+中ドラッグで回転、右ドラッグで回転。",
"nav.inventor.help": "中ドラッグでパン、Shift+中ドラッグで回転、Ctrl+中ドラッグでズーム、右ドラッグで回転。",
"nav.openscad.help": "空きスペースで左ドラッグすると回転、Shift+左ドラッグで矩形選択、スケッチ ジオメトリはドラッグで編集。右または中ドラッグでパン、Shift+右または Shift+中ドラッグでズーム。",
@@ -1456,6 +1486,10 @@
"prefs.design_note": "新しいデザインの初期設定です。開いているデザインはそれぞれの設定を保持します。",
"prefs.devices": "3D マウス",
"prefs.dialog_buttons": "ダイアログのボタン",
"prefs.dialogs": "ダイアログ",
"prefs.dialogs_edge": "ビューの端",
"prefs.dialogs_left_column": "左の列",
"prefs.dialogs_note": "コマンドダイアログを開く場所:ビューの端にドッキングするか、左の列でブラウザーと履歴の代わりに表示します。ダイアログを閉じるとブラウザーと履歴は元どおりに戻ります。",
"prefs.display": "表示",
"prefs.edges": "エッジと陰影",
"prefs.extend": "拡張",
@@ -1515,6 +1549,8 @@
"prefs.opaque": "不透明",
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
"prefs.orientation": "方向",
"prefs.origin_plane_names": "原点平面の名前",
"prefs.origin_plane_names_note": "ブラウザー、ダイアログ、一覧に表示される名前です。{top} は XY、{front} は XZ、{right} は YZ です (Z が上)。コマンド、スクリプト、保存されたデザインでは XY、XZ、YZ のままです。",
"prefs.overall_speed": "全体の速度",
"prefs.placement": "配置",
"prefs.prefer_free_sketch_points": "スケッチ点が重なったときは移動可能な点を優先する",
@@ -1555,6 +1591,12 @@
"prefs.supersampling_brief": "大きく描画して縮小するため、非常に鮮明になりますが負荷も増えます。",
"prefs.tab_accent_note": "アクセント色はアクティブなタブに従います",
"prefs.theme_file": "テーマファイル",
"prefs.theme_follow": "テーマファイルに従う",
"prefs.theme_follow_error": "適用できません: {error}。最後に正常に適用されたテーマを維持します。",
"prefs.theme_follow_launch": "今回の起動時のみ有効です (--theme-file)。別のツールがファイルを書き換えるたびに再読み込みします。",
"prefs.theme_follow_note": "別のツールがファイルを書き換えるたびに再読み込みします。テーマを選ぶと追従を停止します。",
"prefs.theme_follow_off": "オフ",
"prefs.theme_follow_stop": "追従を停止",
"prefs.timeline_half_hour": "−30 分",
"prefs.timeline_now": "現在",
"prefs.timeline_quarter_hour": "−15",
@@ -1619,6 +1661,7 @@
"render.aging_target_search": "ボディ名を絞り込む",
"render.aging_upward": "上向き面への堆積",
"render.all_controls_help": "現在の全コントロールをまとめて表示します。各ページでは同じ要求を保ちながら項目別に編集できます。",
"render.all_shots_label": "すべてのショット",
"render.appearance_physics_separate": "外観の設定では、密度、熱特性などの工学的な物性値は割り当てられません。",
"render.availability_native": "ネイティブ構成。適用前に確認してください",
"render.availability_projection": "対応する部分適用。範囲と省略項目を確認してください",
@@ -1773,10 +1816,13 @@
"render.effects_reset_lens": "レンズ効果をリセット",
"render.effects_seed": "パターンシード",
"render.export_display": "書き出し:表示PNG",
"render.export_image_label": "画像を書き出す",
"render.export_linear": "書き出し:生の線形EXR",
"render.exr_pending": "GPUでは線形EXRを保持できます。圧縮、複数レイヤー、補助パスは未実装です。",
"render.film_grain": "写真粒子",
"render.fit": "製品を画面に合わせる",
"render.fixed_camera": "カメラ",
"render.fixed_world": "ワールド",
"render.flare_ghosts": "光源ごとのゴースト環",
"render.flare_halo": "フレアのハロー",
"render.flare_streak": "アナモルフィック風の筋",
@@ -1803,6 +1849,12 @@
"render.hdr_primaries_srgb": "原色:sRGB/D65",
"render.hdr_primaries_undeclared": "原色が宣言されていません。レンダラーの色要件を確認してください。",
"render.hdr_resource_missing": "保存されたHDRリソースがこのデザインにありません。",
"render.heading_batch": "バッチ",
"render.heading_frame": "フレーム",
"render.heading_lens": "レンズ",
"render.heading_results": "結果",
"render.heading_shots": "ショット",
"render.hide_shots_label": "ショットストリップを非表示",
"render.image_advanced": "GPU 詳細設定",
"render.image_cancelled": "レンダリングをキャンセルしました",
"render.image_cancelling": "キャンセル中",
@@ -1834,6 +1886,7 @@
"render.image_width": "幅",
"render.include_rig_cards": "Include passive cards and flags",
"render.inspect_scene": "記録するシーンの概要を更新",
"render.inspector": "インスペクター",
"render.keep_editing": "編集を続ける",
"render.layer_down": "下へ",
"render.layer_enabled": "層を有効にする",
@@ -1902,6 +1955,8 @@
"render.library_unfavorite": "お気に入りから削除",
"render.library_uniform_ack": "空間変化や高度な効果のない均一近似を使用",
"render.library_usable": "実行可能な部分適用",
"render.light_anchor_pending": "ライトの座標系の変更はまだサポートされていません。",
"render.lights_fixed_to": "ライトの固定先",
"render.linear_help": "ノイズ除去、露出、トーンマッピング、フィルター、背景処理より前の32ビット浮動小数点RGBAを保持します。線形sRGB/D65で、被覆率を乗算した値です。追加メモリを明示的に見積もります。圧縮や追加パスはありません。",
"render.linear_retain": "生の線形EXRを保持",
"render.linear_unavailable": "選択した完了画像にEXRは保持されていません。レンダー前に保持を有効にしてください。古いPNGから生の放射輝度は復元できません。",
@@ -1987,6 +2042,7 @@
"render.output_over_budget": "要求画像が選択したリソース枠を超えています。対応するサイズかプロファイルを明示的に選択してください。自動的な品質低下は行いません。",
"render.output_ratio_refused": "連動する寸法が方式の上限を超えます。以前のサイズを維持します。縦横比固定を解除するか、小さい値を選択してください。",
"render.output_swap_orientation": "幅と高さを交換",
"render.output_tools_inspector": "サイズと形式は出力インスペクターで編集してください。",
"render.page_aging": "エイジング・付着物",
"render.page_all": "全コントロール",
"render.page_appearance": "外観",
@@ -1998,6 +2054,7 @@
"render.page_lighting": "照明",
"render.page_output": "出力",
"render.page_scene": "シーン",
"render.paint_label": "ペイント",
"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": "透視投影カメラ",
@@ -2013,6 +2070,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": "コンポーネント",
"render.pick_label": "選択",
"render.pick_up_label": "取得",
"render.pick_waiting": "製品の選択はまだサポートされていません。",
"render.post_contrast": "コントラスト",
"render.post_gain_b": "青ゲイン",
"render.post_gain_g": "緑ゲイン",
@@ -2025,6 +2086,7 @@
"render.preview": "統合プレビュー",
"render.preview_help": "ラスタシェーディングとブレンド透明度を使用します。屈折、HDRI、最終レンダリングはまだ利用できません。",
"render.preview_image_settings_help": "ライブプレビューは固定のスタジオ照明を使用します。画像の露出と照明は、確認または開始時に画像ジョブへ適用されます。",
"render.preview_lighting_help": "ライブプレビューは直接光のみを表示し、影は表示しません。",
"render.preview_retry": "ライブプレビューを再試行",
"render.preview_unavailable_help": "ライブプレビューは利用できません。保存設定と画像ジョブの操作は使用できます。シーンを変更するか明示的に再試行してください。拒否されたシーンを毎フレーム再構築しません。",
"render.preview_upload": "ライブプレビューの頂点データ量",
@@ -2038,8 +2100,10 @@
"render.radial_distortion": "放射状歪み",
"render.regrade_action": "生マスターを再調整(ノイズ除去なし)",
"render.regrade_help": "選択した完了済み生マスターから、この表示設定で別の PNG を作成します。光線追跡、撮影サイズ・カメラ・照明の変更、EXR の上書きは行いません。ガイドを保持しないためノイズ除去は再適用しません。通常のジョブ操作で書き出せます。",
"render.regrade_label": "再グレーディング",
"render.reload_settings": "保存した設定を再読み込み",
"render.replace_ground": "既存ステージの置換を確認",
"render.replace_label": "置換",
"render.replace_saved_surface": "Replace an existing saved binding for this target",
"render.restore_camera": "保存したカメラを復元",
"render.result_actual_help": "画像の1ピクセルを画面の1ピクセルで表示します。",
@@ -2085,6 +2149,7 @@
"render.sampling_title": "適応・透過サンプリング",
"render.save_continue": "保存して続行",
"render.save_setup": "セットアップを保存",
"render.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",
@@ -2104,6 +2169,9 @@
"render.setup_runtime_unsupported": "このジョブにはマップまたはシーンのボディへの割り当てが含まれるため、全体を保存できません。移植可能な設定を保存する前に、一時入力を削除してください。",
"render.setup_save_missing": "コマンド完了後に保存済みレンダリング設定を見つけられませんでした。",
"render.sharpening": "ディテールのシャープ化",
"render.shell_tab_appearance": "外観",
"render.shell_tab_camera": "カメラ",
"render.shell_tab_scene": "シーン",
"render.shot_duplicate": "保存済みショットを複製",
"render.shot_manager": "保存済みショット",
"render.shot_new_name": "新しいショット名",
@@ -2112,11 +2180,19 @@
"render.shot_remove_title": "保存済みショットを削除しますか?",
"render.shot_rename": "保存済みショットの名前を変更",
"render.shot_saved_only": "先に現在の下書きを保存してください。複製と名前変更では正確な保存オプションとリソース参照を保持します。",
"render.show_shots_label": "ショットストリップを表示",
"render.sky_ground": "地面・線形RGB",
"render.sky_horizon": "地平線・線形RGB",
"render.sky_zenith": "天頂・線形RGB",
"render.soft_focus": "ソフトフォーカス",
"render.stage_frame": "構図",
"render.stage_light": "照明",
"render.stage_look": "外観",
"render.stage_orthographic": "平行投影",
"render.stage_output": "出力",
"render.stage_panel_count": "Saved panels",
"render.stage_preview_note": "統合プレビュー · 直接光",
"render.strip_label": "ストリップ",
"render.studio_add_area": "エリアライトを追加",
"render.studio_ambient": "一定の環境放射輝度",
"render.studio_ambient_rgb": "一定の環境放射輝度(リニアRGB)",
@@ -2173,6 +2249,12 @@
"render.volume_max": "最大境界 · ワールド mm",
"render.volume_min": "最小境界 · ワールド mm",
"render.volume_title": "有限の均一ボリューム",
"render.wait_batch": "未対応: 保存ショットのレンダーキュー",
"render.wait_compare": "未対応: 保持された画像の比較",
"render.wait_paint": "未対応: 製品の選択と外観のペイント",
"render.wait_pick_up": "未対応: 製品の選択と外観の採取",
"render.wait_replace": "未対応: 外観のすべての使用箇所の置換",
"render.wait_solo": "未対応: ライトの表示とソロ操作",
"sample.bracket.about": "2 mm の板金ルールによるベース フランジ、エッジ フランジ、ヘミング",
"sample.bracket.name": "板金ブラケット",
"sample.enclosure.about": "リップ、ねじボス、レスト、スナップ フィットを備えたシェル化した直方体",
@@ -2264,6 +2346,7 @@
"timeline.delete_after_marker_one": "履歴マーカー以降の 1 個のフィーチャーを削除",
"timeline.delete_after_marker_other": "履歴マーカー以降の {count} 個のフィーチャーを削除",
"timeline.delete_folders_tip": "フィーチャーと入れ子のフォルダーを履歴に残します",
"timeline.delete_folders_with_features_tip": "フォルダーを、中のすべてのフィーチャーと入れ子のフォルダーごと削除します",
"timeline.delete_many": "選択した {count} 個のフィーチャーを削除",
"timeline.feature_error": "エラー: {message}",
"timeline.feature_hint": "ダブルクリックで編集 · ドラッグで並べ替え · 右クリックでその他の操作",
@@ -2273,6 +2356,7 @@
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
"timeline.rename_group": "フィーチャーフォルダの名前を変更",
"timeline.reorder_many": "フィーチャーの順序を変更",
"timeline.suppress_folders_tip": "フォルダーと入れ子のフォルダー内のすべてのフィーチャーを抑制、または抑制を解除します",
"timeline.ungroup": "フィーチャーフォルダを削除",
"to the face": "面まで",
"to the next face": "次の面まで",
@@ -2283,7 +2367,11 @@
"toolbar.render_camera": "カメラ",
"toolbar.render_environment": "環境",
"toolbar.render_lighting": "照明",
"toolbar.render_lights": "ライト",
"toolbar.render_result": "結果",
"toolbar.render_scene": "シーン",
"toolbar.render_stage": "ステージ",
"toolbar.render_studio": "スタジオ",
"trace.points": "経路上の {count} 点",
"trace.stopped_by_contact": "サンプル {sample} で終了: {a} が {b} を通り抜けてしまいます",
"trace.stopped_by_loop": "サンプル {sample} で終了: {joint} を通るループはこれ以上閉じられません",
@@ -2462,6 +2550,7 @@
"ui.agent_chat_import_failed": "{name}: インポートに失敗しました: {error}",
"ui.agent_chat_intro": "普段使っているコーディングエージェント、または選んだモデルのエンドポイントとチャットできます。SolidCraft のツールで開いているデザインを操作し、変更はその場で表示され、メッセージごとに1回で元に戻せます。確認なしで許可する範囲は「環境設定 > AI」で設定します。",
"ui.agent_chat_key_in_env": "キー設定済み",
"ui.agent_chat_last_missing": "前回使用したエージェント ({agent}) は利用できなくなりました。エージェントを選んでから「再試行」を押してください。",
"ui.agent_chat_left": "左",
"ui.agent_chat_limit": "エージェントが1メッセージあたりの変更上限 {limit} に達しました。続けさせるには、もう一度メッセージを送ってください。",
"ui.agent_chat_login_found": "ログイン情報あり",
@@ -2508,6 +2597,7 @@
"ui.agent_chat_remove": "待機から削除",
"ui.agent_chat_remove_file": "このメッセージから外す",
"ui.agent_chat_remove_imported": "このメッセージから外します。パーツはデザインに残ります (元に戻すで削除できます)",
"ui.agent_chat_retry": "再試行",
"ui.agent_chat_right": "右",
"ui.agent_chat_save": "保存",
"ui.agent_chat_send": "送信",
@@ -2520,6 +2610,7 @@
"ui.agent_chat_state_auth": "サインインが必要",
"ui.agent_chat_state_busy": "作業中…",
"ui.agent_chat_state_exited": "エージェント停止",
"ui.agent_chat_state_failed": "接続に失敗しました",
"ui.agent_chat_state_off": "未接続",
"ui.agent_chat_state_ready": "準備完了",
"ui.agent_chat_state_starting": "起動中…",
@@ -2823,6 +2914,7 @@
"ui.fully_constrained": "完全拘束",
"ui.function": "関数",
"ui.general": "一般",
"ui.gpu_memory_lowered": "グラフィックカードのメモリが不足しています。ビューは低い画質で描画されます。",
"ui.graphics": "グラフィックス",
"ui.graphics_custom": "カスタム",
"ui.graphics_performance": "パフォーマンス",
@@ -3093,6 +3185,7 @@
"ui.render": "レンダリング",
"ui.renderer": "レンダラー",
"ui.repeat": "繰り返し",
"ui.repeat_nothing": "まだ繰り返すものがありません。先にツールかダイアログを開始してください。",
"ui.replace_with_link_done": "コピーはリンクされたデザインの配置になりました。",
"ui.report_a_bug": "不具合を報告",
"ui.reset": "リセット",
@@ -3315,5 +3408,6 @@
"web.open_link": "リンク先のデザインを開きますか?",
"web.open_link_confirm": "保存していない変更があります。リンク先のデザインを新しいタブで開きますか?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 選択 · Enter / Tab 補完 · Esc 閉じる",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 選択 · Enter 入力どおりに適用 · Esc 閉じる"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 選択 · Enter 入力どおりに適用 · Esc 閉じる",
"⌘Enter continues on GitHub.": "⌘EnterでGitHubに進みます。"
}
+72
View File
@@ -19,9 +19,11 @@
"Add Mid Radius": "중간 반지름 추가",
"Add Row": "행 추가",
"Add a tool": "도구 추가",
"Add all bodies": "모든 바디 추가",
"Add condition": "조건 추가",
"Add or edit rule": "규칙 추가 또는 편집",
"Add to New Folder": "새 폴더에 추가",
"Add visible bodies": "보이는 바디 추가",
"Advanced": "고급",
"Against": "상대",
"Alignment": "정렬",
@@ -373,6 +375,7 @@
"Remove this point": "이 점 제거",
"Rename Feature": "피처 이름 바꾸기",
"Rename Folder": "폴더 이름 바꾸기",
"Render Stage": "렌더링 단계",
"Replace existing library file": "기존 라이브러리 파일 바꾸기",
"Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기",
"Report text copied. Paste it into GitHub and submit your report there.": "보고서 텍스트를 복사했습니다. GitHub에 붙여넣고 그곳에서 보고서를 제출하세요.",
@@ -598,6 +601,9 @@
"cmd.appearance_assign": "외관",
"cmd.appearance_library": "외관 라이브러리",
"cmd.appearance_list": "외관 목록",
"cmd.appearance_paint": "모양 칠하기",
"cmd.appearance_pick_up": "모양 가져오기",
"cmd.appearance_replace": "모양 바꾸기",
"cmd.assembly_bom": "자재 명세서",
"cmd.body_rename": "바디 이름 변경",
"cmd.browser_group": "새 그룹",
@@ -677,6 +683,7 @@
"cmd.document_metadata_set": "설계 속성 변경",
"cmd.document_units": "설계 단위 변경",
"cmd.edit_checkpoint": "체크포인트",
"cmd.edit_repeat": "마지막 명령 반복",
"cmd.edit_restore_checkpoint": "체크포인트 복원",
"cmd.engine_commands": "명령 목록",
"cmd.engine_describe": "명령 설명",
@@ -782,9 +789,11 @@
"cmd.plastic_rules": "플라스틱 규칙 목록",
"cmd.plastic_snap_fit": "스냅핏",
"cmd.render_backends": "렌더링 백엔드",
"cmd.render_batch": "모든 샷 렌더링",
"cmd.render_camera_fit": "렌더 대상 화면 맞춤",
"cmd.render_camera_get": "렌더 카메라 확인",
"cmd.render_cancel": "렌더링 취소",
"cmd.render_compare": "이미지 비교",
"cmd.render_export": "PNG 내보내기",
"cmd.render_hdr_inspect": "HDR 소스 검사",
"cmd.render_library_appearance_apply": "라이브러리 불투명 외관 적용",
@@ -798,6 +807,7 @@
"cmd.render_library_surface_prepare": "제작된 표면 맵 준비",
"cmd.render_library_surface_unbind": "Remove Surface Binding",
"cmd.render_library_targets": "렌더 리소스 대상 찾기",
"cmd.render_lights_solo": "광원 단독 표시",
"cmd.render_preflight": "렌더 요청 확인",
"cmd.render_result_pin": "표시된 렌더 결과 유지",
"cmd.render_scene": "렌더링 장면 검사",
@@ -949,6 +959,7 @@
"cmd.view_normal_to": "선택 항목에 수직",
"cmd.view_rename": "이름 있는 보기 이름 변경",
"cmd.view_save": "새 이름 있는 보기",
"cmd.view_shots_strip": "샷 스트립",
"color.copy": "클릭하여 색상 값 복사",
"color.edit": "클릭하여 색상 편집",
"color.gamma": "색상 값을 0-255 감마 공간으로 표시 중",
@@ -1136,6 +1147,8 @@
"manage.material_hint": "바디를 선택하고 물리 재질을 지정하세요.",
"manage.parameters_hint": "표현식과 단위를 편집하세요. Enter를 누르거나 필드를 벗어나면 변경 사항이 적용됩니다.",
"manage.rules_hint": "편집할 규칙을 선택하거나 새 규칙을 만드세요. 확인하면 규칙이 저장됩니다.",
"material.selection.choose": "재질 선택",
"material.selection.mixed": "서로 다른 재질",
"measure.at": "위치",
"measure.item": "항목 {number}",
"messages.ago_h": "{n}시간 전",
@@ -1165,6 +1178,8 @@
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
"nav.free_orbit.label": "자유 회전",
"nav.free_orbit.snap_help": "마우스와 3D 마우스로 양쪽 극점을 지나 회전할 수 있으며 롤 각도가 유지됩니다. 궤도 회전 드래그 중 Shift를 누르면 보기 방향과 롤 각도가 스냅됩니다.",
"nav.free_orbit_upright.help": "마우스를 좌우로 드래그하면 모델이 월드의 수직 축을 중심으로 회전하고, 위아래로 드래그하면 제한 없이 극점을 넘어 기울어집니다.",
"nav.free_orbit_upright.label": "수직 축을 중심으로 회전",
"nav.fusion.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, 오른쪽 드래그는 회전입니다",
"nav.inventor.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, Ctrl+가운데 드래그는 확대/축소, 오른쪽 드래그는 회전입니다",
"nav.openscad.help": "빈 공간에서 왼쪽 드래그는 회전, Shift+왼쪽 드래그는 상자 선택, 스케치 형상 드래그는 편집입니다. 오른쪽 또는 가운데 드래그는 이동, Shift+오른쪽 또는 Shift+가운데 드래그는 확대/축소입니다",
@@ -1224,6 +1239,10 @@
"prefs.design_note": "새 설계가 시작할 때의 설정입니다. 열려 있는 설계는 자체 설정을 유지합니다.",
"prefs.devices": "3D 마우스",
"prefs.dialog_buttons": "대화 상자 버튼",
"prefs.dialogs": "대화 상자",
"prefs.dialogs_edge": "뷰 가장자리",
"prefs.dialogs_left_column": "왼쪽 열",
"prefs.dialogs_note": "명령 대화 상자가 열리는 위치: 뷰 가장자리에 고정하거나, 왼쪽 열에서 브라우저와 이력 대신 표시합니다. 대화 상자를 닫으면 브라우저와 이력이 그대로 돌아옵니다.",
"prefs.display": "표시",
"prefs.edges": "모서리 및 음영",
"prefs.extend": "확장",
@@ -1271,6 +1290,8 @@
"prefs.opaque": "불투명",
"prefs.orbit_snap": "회전하는 동안 Shift를 누르면 축에 맞춰집니다.",
"prefs.orientation": "방향",
"prefs.origin_plane_names": "원점 평면 이름",
"prefs.origin_plane_names_note": "브라우저, 대화 상자, 목록에 표시되는 이름입니다. {top}은 XY, {front}는 XZ, {right}는 YZ이며 Z가 위쪽입니다. 명령, 스크립트, 저장된 디자인은 XY, XZ, YZ를 유지합니다.",
"prefs.overall_speed": "전체 속도",
"prefs.preview_choices": "선택 항목 미리보기",
"prefs.quality_default": "기본값",
@@ -1304,6 +1325,12 @@
"prefs.supersampling_brief": "뷰를 더 크게 그린 뒤 줄여서 필터링합니다. 매우 선명하지만 더 무겁습니다.",
"prefs.tab_accent_note": "강조 색상이 활성 탭을 따라갑니다",
"prefs.theme_file": "테마 파일",
"prefs.theme_follow": "테마 파일 따르기",
"prefs.theme_follow_error": "적용되지 않음: {error}. 마지막으로 적용된 정상 테마를 유지합니다.",
"prefs.theme_follow_launch": "이번 실행에만 적용됩니다 (--theme-file). 다른 도구가 파일을 다시 쓸 때마다 다시 불러옵니다.",
"prefs.theme_follow_note": "다른 도구가 파일을 다시 쓸 때마다 다시 불러옵니다. 테마를 선택하면 따르기를 중지합니다.",
"prefs.theme_follow_off": "끄기",
"prefs.theme_follow_stop": "따르기 중지",
"prefs.timeline_half_hour": "−30분",
"prefs.timeline_now": "지금",
"prefs.timeline_quarter_hour": "−15",
@@ -1331,6 +1358,7 @@
"recovery.min_ago": "{count}분 전",
"recovery.never_saved": "저장된 적 없음",
"render.add_ground": "Add fitted ground to setup",
"render.all_shots_label": "모든 샷",
"render.background": "장면 배경",
"render.bloom_enable": "표시 블룸",
"render.bloom_help": "양의 선형 하이라이트를 세 번의 정규화 필터로 확산하는 출력 전용 효과입니다. 반경은 출력 픽셀 단위이며 sRGB 전에 더합니다. 렌즈·볼륨·수렴 개선이 아니며 원본 EXR은 변하지 않습니다.",
@@ -1350,8 +1378,17 @@
"render.cpu_budget_studio": "스튜디오 (큰 이미지)",
"render.cpu_budget_triangles": "삼각형 수 상한",
"render.cpu_budget_workstation": "워크스테이션 (대규모 장면)",
"render.export_image_label": "이미지 내보내기",
"render.exr_pending": "GPU는 원본 선형 EXR을 보존할 수 있습니다. 압축, 다중 레이어 이미지 및 보조 렌더 패스는 아직 사용할 수 없습니다.",
"render.fit": "제품에 맞추기",
"render.fixed_camera": "카메라",
"render.fixed_world": "월드",
"render.heading_batch": "일괄 처리",
"render.heading_frame": "프레임",
"render.heading_lens": "렌즈",
"render.heading_results": "결과",
"render.heading_shots": "샷",
"render.hide_shots_label": "샷 스트립 숨기기",
"render.image_cancelled": "렌더링 취소됨",
"render.image_cancelling": "취소 중",
"render.image_completed": "이미지 내보내기 준비됨",
@@ -1375,6 +1412,7 @@
"render.image_use_largest": "이 비율에서 가장 큰 크기 사용:",
"render.image_width": "너비",
"render.include_rig_cards": "Include passive cards and flags",
"render.inspector": "속성 검사기",
"render.library_applied": "리소스 변환을 적용했습니다. 시작 전에 전체 렌더 요청을 확인하세요.",
"render.library_apply_hdr": "이 HDR 환경과 함께 저장",
"render.library_apply_maps": "제작된 GPU 표면 맵 사용",
@@ -1410,6 +1448,8 @@
"render.library_target": "대상 바디",
"render.library_title": "렌더 리소스 라이브러리",
"render.library_uniform_ack": "공간 변화나 고급 효과 없는 균일 근사 사용",
"render.light_anchor_pending": "조명 좌표계 변경은 아직 지원되지 않습니다.",
"render.lights_fixed_to": "조명 고정 기준",
"render.material_color_replace": "기본 맵으로 바디 색상 교체",
"render.material_limits": "최대 128개 바디 설정, 분리된 유리 32개, 코팅 64개, 텍스처 연결 128개. 같은 맵은 메모리를 공유하며 실제 장치 및 전체 메모리 제한은 유지됩니다.",
"render.material_metallic_map": "금속성 맵 (선형)",
@@ -1419,6 +1459,8 @@
"render.opaque_image_summary": "미리보기 PNG · 불투명 PBR · 블렌딩 투명도",
"render.opaque_preview_help": "불투명 PBR 및 단색 표시, 블렌딩 투명도를 지원합니다. 굴절, HDRI 및 최종 렌더링은 아직 지원하지 않습니다.",
"render.output_format_status": "PNG는 완료된 표시 이미지 그대로입니다. 선택적 GPU EXR은 표시 필터 전 선형 FLOAT32 방사 휘도를 보존합니다. 미리보기 캐시나 다른 작업으로 바꾸어 내보내지 않습니다.",
"render.output_tools_inspector": "출력 검사기에서 크기와 형식을 편집하세요.",
"render.paint_label": "칠하기",
"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": "원근 카메라",
@@ -1431,9 +1473,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": "구성요소",
"render.pick_label": "선택",
"render.pick_up_label": "가져오기",
"render.pick_waiting": "제품 선택은 아직 지원되지 않습니다.",
"render.preview": "통합 미리보기",
"render.preview_help": "래스터 셰이딩과 블렌딩 투명도를 사용합니다. 굴절, HDRI 및 최종 렌더링은 아직 지원하지 않습니다.",
"render.preview_lighting_help": "실시간 미리 보기에는 직접 조명만 표시되며 그림자는 표시되지 않습니다.",
"render.regrade_label": "색상 재조정",
"render.reload_settings": "저장된 설정 다시 불러오기",
"render.replace_label": "바꾸기",
"render.replace_saved_surface": "Replace an existing saved binding for this target",
"render.restore_camera": "저장된 카메라 복원",
"render.result_reduced_display": "표시 미리보기를 뷰포트 장치 또는 캐시 제한에 맞춰 축소했습니다. 보관된 PNG와 내보내기는 전체 해상도를 유지합니다. 이 축소 보기에서는 원본 100% 검사가 불가능합니다.",
@@ -1460,15 +1509,33 @@
"render.ribbon_studios_help": "구현된 스튜디오를 찾아보고 적용 전에 교체 항목을 검토합니다. 개념 장면은 구분됩니다.",
"render.ribbon_surface_maps": "표면 맵",
"render.save_setup": "설정 저장",
"render.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",
"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": "설정 이름",
"render.shell_tab_appearance": "모양",
"render.shell_tab_camera": "카메라",
"render.shell_tab_scene": "장면",
"render.show_shots_label": "샷 스트립 표시",
"render.stage_frame": "구도",
"render.stage_light": "조명",
"render.stage_look": "외관",
"render.stage_orthographic": "직교 투영",
"render.stage_output": "출력",
"render.stage_panel_count": "Saved panels",
"render.stage_preview_note": "통합 미리 보기 · 직접광",
"render.strip_label": "스트립",
"render.unavailable_preview": "이 설정은 사용할 수 없는 미리보기 프로필을 요청합니다.",
"render.unsaved": "저장하지 않은 미리보기 변경 사항",
"render.use_preview": "통합 미리보기 사용",
"render.wait_batch": "아직 지원되지 않음: 저장된 샷 렌더 대기열",
"render.wait_compare": "아직 지원되지 않음: 보관된 이미지 비교",
"render.wait_paint": "아직 지원되지 않음: 제품 선택 및 모양 페인팅",
"render.wait_pick_up": "아직 지원되지 않음: 제품 선택 및 모양 샘플링",
"render.wait_replace": "아직 지원되지 않음: 모양이 사용된 모든 곳 바꾸기",
"render.wait_solo": "아직 지원되지 않음: 조명 표시 및 단독 표시 제어",
"sample.bracket.about": "2 mm 판금 규칙을 적용한 베이스 플랜지, 에지 플랜지, 헴",
"sample.bracket.name": "판금 브래킷",
"sample.enclosure.about": "리프, 나사 보스, 레스트, 스냅핏이 있는 쉘 처리된 박스",
@@ -1558,7 +1625,11 @@
"toolbar.render_camera": "카메라",
"toolbar.render_environment": "환경",
"toolbar.render_lighting": "조명",
"toolbar.render_lights": "조명",
"toolbar.render_result": "결과",
"toolbar.render_scene": "장면",
"toolbar.render_stage": "스테이지",
"toolbar.render_studio": "스튜디오",
"trace.points": "경로를 따라 {count}개 점",
"trace.stopped_by_contact": "샘플 {sample}에서 끝남: {a}이(가) {b}을(를) 통과합니다",
"trace.stopped_by_loop": "샘플 {sample}에서 끝남: {joint}을(를) 지나는 루프를 더 이상 닫을 수 없습니다",
@@ -2290,6 +2361,7 @@
"ui.render": "렌더",
"ui.renderer": "렌더러",
"ui.repeat": "반복",
"ui.repeat_nothing": "아직 반복할 것이 없습니다. 먼저 도구나 대화 상자를 시작하세요.",
"ui.report_a_bug": "버그 신고",
"ui.reset": "재설정",
"ui.reset_all": "모두 재설정",
+6
View File
@@ -1003,6 +1003,12 @@
"pick one or two vertices, edges or faces": "pilih satu atau dua bucu, tepi atau muka",
"prefs.cpu": "CPU (perisian)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Ikut fail tema",
"prefs.theme_follow_error": "Tidak digunakan: {error}. Tema sah terakhir kekal aktif.",
"prefs.theme_follow_launch": "Diikuti untuk sesi ini sahaja (--theme-file); dimuat semula setiap kali alat lain menulis semula fail.",
"prefs.theme_follow_note": "Dimuat semula setiap kali alat lain menulis semula fail. Memilih tema menghentikan ikutan.",
"prefs.theme_follow_off": "Mati",
"prefs.theme_follow_stop": "Berhenti mengikuti",
"properties.title": "Sifat: {name}",
"recovery.days_ago": "{count} hari lalu",
"recovery.detail": "{file} · {features} ciri · disimpan automatik {when}",
+6
View File
@@ -1270,6 +1270,12 @@
"prefs.supersampling_brief": "Tegner visningen større og skalerer den ned: svært skarpt, og mer krevende.",
"prefs.tab_accent_note": "Aksentfargen følger den aktive fanen",
"prefs.theme_file": "temafil",
"prefs.theme_follow": "Følg en temafil",
"prefs.theme_follow_error": "Ikke brukt: {error}. Det siste gyldige temaet forblir aktivt.",
"prefs.theme_follow_launch": "Følges bare denne kjøringen (--theme-file); lastes inn på nytt når et annet verktøy skriver om filen.",
"prefs.theme_follow_note": "Lastes inn på nytt når et annet verktøy skriver om filen. Valg av et tema stopper dette.",
"prefs.theme_follow_off": "Av",
"prefs.theme_follow_stop": "Slutt å følge",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nå",
"prefs.timeline_quarter_hour": "−15",
+10
View File
@@ -1001,7 +1001,17 @@
"pick a hole to seat it, or OK places it at the origin": "kies een gat om het te plaatsen, of OK plaatst het op de oorsprong",
"pick one or two vertices, edges or faces": "kies een of twee hoekpunten, randen of vlakken",
"prefs.cpu": "CPU (software)",
"prefs.dialogs": "Dialogen",
"prefs.dialogs_edge": "Aan de rand van de weergave",
"prefs.dialogs_left_column": "In de linkerkolom",
"prefs.dialogs_note": "Waar opdrachtdialogen openen: vastgezet aan de rand van de weergave, of in de linkerkolom in plaats van de browser en de geschiedenis, die terugkomen zoals ze waren wanneer de dialoog sluit.",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Een themabestand volgen",
"prefs.theme_follow_error": "Niet toegepast: {error}. Het laatste geldige thema blijft actief.",
"prefs.theme_follow_launch": "Alleen deze sessie gevolgd (--theme-file); opnieuw geladen zodra een ander hulpmiddel het bestand overschrijft.",
"prefs.theme_follow_note": "Opnieuw geladen zodra een ander hulpmiddel het bestand overschrijft. Een thema kiezen stopt het volgen.",
"prefs.theme_follow_off": "Uit",
"prefs.theme_follow_stop": "Volgen stoppen",
"properties.title": "Eigenschappen: {name}",
"recovery.days_ago": "{count} dagen geleden",
"recovery.detail": "{file} · {features} kenmerken · automatisch opgeslagen {when}",
+6
View File
@@ -1173,6 +1173,12 @@
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
"prefs.theme_file": "plik motywu",
"prefs.theme_follow": "Śledź plik motywu",
"prefs.theme_follow_error": "Nie zastosowano: {error}. Ostatni poprawny motyw pozostaje aktywny.",
"prefs.theme_follow_launch": "Śledzenie tylko w tej sesji (--theme-file); plik jest wczytywany ponownie po każdym nadpisaniu przez inne narzędzie.",
"prefs.theme_follow_note": "Plik jest wczytywany ponownie po każdym nadpisaniu przez inne narzędzie. Wybór motywu kończy śledzenie.",
"prefs.theme_follow_off": "Wyłączone",
"prefs.theme_follow_stop": "Zakończ śledzenie",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "teraz",
"prefs.timeline_quarter_hour": "−15",
+6
View File
@@ -1293,6 +1293,12 @@
"prefs.supersampling_brief": "Desenha a vista maior e a reduz com filtragem: muito nítida, e mais pesada.",
"prefs.tab_accent_note": "A cor de destaque acompanha a aba ativa",
"prefs.theme_file": "arquivo de tema",
"prefs.theme_follow": "Seguir um arquivo de tema",
"prefs.theme_follow_error": "Não aplicado: {error}. O último tema válido continua ativo.",
"prefs.theme_follow_launch": "Seguido apenas nesta sessão (--theme-file); recarregado sempre que outra ferramenta reescreve o arquivo.",
"prefs.theme_follow_note": "Recarregado sempre que outra ferramenta reescreve o arquivo. Escolher um tema interrompe o acompanhamento.",
"prefs.theme_follow_off": "Desativado",
"prefs.theme_follow_stop": "Parar de seguir",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "agora",
"prefs.timeline_quarter_hour": "−15",
+6
View File
@@ -1002,6 +1002,12 @@
"pick one or two vertices, edges or faces": "alegeți unul sau două vârfuri, muchii sau fețe",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Urmărește un fișier de temă",
"prefs.theme_follow_error": "Nu s-a aplicat: {error}. Ultima temă validă rămâne activă.",
"prefs.theme_follow_launch": "Urmărit doar în această sesiune (--theme-file); reîncărcat când alt instrument rescrie fișierul.",
"prefs.theme_follow_note": "Reîncărcat când alt instrument rescrie fișierul. Alegerea unei teme oprește urmărirea.",
"prefs.theme_follow_off": "Dezactivat",
"prefs.theme_follow_stop": "Oprește urmărirea",
"properties.title": "Proprietăți: {name}",
"recovery.days_ago": "acum {count} zile",
"recovery.detail": "{file} · {features} operații · salvat automat {when}",
+6
View File
@@ -1002,6 +1002,12 @@
"pick one or two vertices, edges or faces": "выберите одну-две вершины, ребра или грани",
"prefs.cpu": "ЦП (программный)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Следить за файлом темы",
"prefs.theme_follow_error": "Не применено: {error}. Последняя рабочая тема остаётся активной.",
"prefs.theme_follow_launch": "Только для этого запуска (--theme-file); перезагружается, когда другой инструмент перезаписывает файл.",
"prefs.theme_follow_note": "Перезагружается, когда другой инструмент перезаписывает файл. Выбор темы прекращает отслеживание.",
"prefs.theme_follow_off": "Выкл.",
"prefs.theme_follow_stop": "Прекратить отслеживание",
"properties.title": "Свойства: {name}",
"recovery.days_ago": "{count} дн. назад",
"recovery.detail": "{file} · операций: {features} · автосохранение {when}",
+6
View File
@@ -1002,6 +1002,12 @@
"pick one or two vertices, edges or faces": "välj ett eller två hörn, kanter eller ytor",
"prefs.cpu": "CPU (programvara)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Följ en temafil",
"prefs.theme_follow_error": "Inte tillämpat: {error}. Det senaste giltiga temat förblir aktivt.",
"prefs.theme_follow_launch": "Följs endast denna körning (--theme-file); läses in igen när ett annat verktyg skriver om filen.",
"prefs.theme_follow_note": "Läses in igen när ett annat verktyg skriver om filen. Att välja ett tema avslutar detta.",
"prefs.theme_follow_off": "Av",
"prefs.theme_follow_stop": "Sluta följa",
"properties.title": "Egenskaper: {name}",
"recovery.days_ago": "för {count} dagar sedan",
"recovery.detail": "{file} · {features} funktioner · autosparad {when}",
+1287 -1
View File
File diff suppressed because it is too large Load Diff
+982 -1
View File
File diff suppressed because it is too large Load Diff
+95 -1
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "Thêm bán kính giữa",
"Add Row": "Thêm hàng",
"Add a tool": "Thêm công cụ",
"Add all bodies": "Thêm tất cả thân khối",
"Add bend": "Thêm góc uốn",
"Add condition": "Thêm điều kiện",
"Add or edit rule": "Thêm hoặc sửa quy tắc",
"Add to New Folder": "Thêm vào thư mục mới",
"Add visible bodies": "Thêm các thân khối đang hiện",
"Advanced": "Nâng cao",
"Against": "So với",
"Alignment": "Căn chỉnh",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "BỀ MẶT BIÊN DẠNG",
"BROWSER": "CÂY ĐỐI TƯỢNG",
"Bar": "Thép thanh",
"Base Center": "Tâm đáy",
"Bend": "Góc uốn",
"Bend Edges": "Cạnh uốn gập",
"Bend Line Position": "Vị trí đường chấn",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "Tạo Issue — xem lại bản nháp",
"Create Issue…": "Tạo Issue…",
"Cross curves": "Đường cong giao cắt",
"Ctrl+Enter continues on GitHub.": "Nhấn Ctrl+Enter để tiếp tục trên GitHub.",
"Cube": "Khối lập phương",
"Current orthographic view (1:1 mm)": "Góc nhìn trực giao hiện tại (1:1 mm)",
"Current perspective view": "Góc nhìn phối cảnh hiện tại",
@@ -152,6 +156,7 @@
"Customize…": "Tùy chỉnh…",
"Cylindrical face": "Mặt trụ",
"Delete": "Xóa",
"Delete Folders and Features…": "Xóa thư mục cùng các tính năng…",
"Delete Selected Features…": "Xóa các tính năng đã chọn…",
"Delete Selected Folders": "Xóa các thư mục đã chọn",
"Delete all": "Xóa tất cả",
@@ -430,6 +435,7 @@
"Remove this point": "Xóa điểm này",
"Rename Feature": "Đổi tên tính năng",
"Rename Folder": "Đổi tên thư mục",
"Render Stage": "Giai đoạn kết xuất",
"Reopen designs": "Mở lại thiết kế",
"Reopen the designs that were open when SolidCraft was last closed": "Mở lại các thiết kế đã mở khi đóng SolidCraft lần trước",
"Replace existing library file": "Thay thế tệp thư viện hiện có",
@@ -534,6 +540,7 @@
"Style": "Kiểu hiển thị",
"Summary": "Tóm tắt",
"Suppress": "Bỏ kích hoạt (Suppress)",
"Suppress Selected Folders": "Bỏ kích hoạt các thư mục đã chọn",
"Suppressed": "Bị tạm nén",
"Surface": "Bề mặt",
"Surface, or faces to turn over": "Mặt cong, hoặc các mặt cần lật hướng",
@@ -581,6 +588,7 @@
"UNTRIM": "HỦY CẮT TỈA (UNTRIM)",
"Unit": "Đơn vị",
"Unsuppress": "Kích hoạt lại (Unsuppress)",
"Unsuppress Selected Folders": "Kích hoạt lại các thư mục đã chọn",
"Updating the preview": "Đang cập nhật hình xem trước",
"Use active author placement": "Sử dụng vị trí tác giả đang kích hoạt",
"Use fixed plane": "Sử dụng mặt phẳng cố định",
@@ -628,6 +636,8 @@
"ai.prefs.background_commands": "Chạy các lệnh của agent dưới nền",
"ai.prefs.connect_apps": "Cho phép các ứng dụng AI trên máy tính này kết nối với SolidCraft",
"ai.prefs.connect_apps_note": "Khi SolidCraft khởi động, tự động thêm phần mềm dưới dạng MCP server vào các ứng dụng AI tìm thấy trên máy tính này (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code) để AI agent có thể sử dụng ngay mà không cần cấu hình thủ công. Chỉ mục cấu hình solidcraft sẽ được thêm mới hoặc làm mới đường dẫn; các mục bạn tự tùy chỉnh sẽ được giữ nguyên và file được sao lưu .bak trước khi sửa. Tắt: các mục đã thêm sẽ được gỡ bỏ. Trợ giúp > Kết nối trợ lý ảo AI… để thực hiện thủ công.",
"ai.prefs.connect_at_start": "Kết nối tác tử dùng gần nhất khi SolidCraft khởi động",
"ai.prefs.connect_at_start_note": "Mỗi lần SolidCraft khởi động, chạy ngầm tác tử bạn kết nối gần nhất (hoặc tác tử mặc định) để sẵn sàng trò chuyện. Không áp dụng khi SolidCraft được khởi động với --control. Nếu không kết nối được, Trợ lý hiển thị “Kết nối thất bại” và nút “Thử lại”.",
"ai.prefs.follow_camera": "Xoay camera theo phác thảo",
"ai.prefs.follow_camera_off": "Tự động xoay camera theo phác thảo đang tắt.",
"ai.prefs.follow_camera_on": "Camera sẽ tự động xoay vuông góc với phác thảo do agent tạo.",
@@ -635,6 +645,8 @@
"ai.prefs.make_default": "Đặt làm mặc định",
"ai.prefs.no_agents": "Chưa có agent nào. Thêm một agent hoặc làm mới để tìm các agent đã cài đặt.",
"ai.prefs.no_saved_approvals": "Chưa có phê duyệt nào được lưu.",
"ai.prefs.open_at_start": "Mở bảng Trợ lý khi SolidCraft khởi động",
"ai.prefs.open_at_start_note": "Hiển thị bảng Trợ lý (hoặc cửa sổ riêng của nó) mỗi lần SolidCraft khởi động. Không áp dụng khi SolidCraft được khởi động với --control.",
"ai.prefs.override_all_warning": "Agent này có thể lưu, ghi đè, xuất hoặc thay thế thiết kế mà không cần hỏi trước.",
"ai.prefs.permission_all": "Cho phép tất cả",
"ai.prefs.permission_all_summary": "Các công cụ SolidCraft khả dụng sẽ chạy mà không cần hỏi trước.",
@@ -679,6 +691,9 @@
"cmd.appearance_assign": "Vẻ ngoài / Màu sắc",
"cmd.appearance_library": "Thư viện màu sắc bề mặt",
"cmd.appearance_list": "Danh sách màu sắc bề mặt",
"cmd.appearance_paint": "Tô diện mạo",
"cmd.appearance_pick_up": "Lấy diện mạo",
"cmd.appearance_replace": "Thay diện mạo",
"cmd.assembly_bom": "Bảng kê vật tư (BOM)",
"cmd.body_rename": "Đổi tên thân khối",
"cmd.browser_collapse_all": "Thu gọn tất cả",
@@ -766,6 +781,7 @@
"cmd.document_revision": "Phiên bản thiết kế",
"cmd.document_units": "Thay đổi đơn vị thiết kế",
"cmd.edit_checkpoint": "Điểm kiểm tra (Checkpoint)",
"cmd.edit_repeat": "Lặp lại lệnh trước",
"cmd.edit_restore_checkpoint": "Khôi phục điểm kiểm tra",
"cmd.engine_commands": "Danh sách lệnh hệ thống",
"cmd.engine_describe": "Mô tả lệnh",
@@ -879,9 +895,11 @@
"cmd.plastic_snap_fit": "Khớp gài bấm (Snap Fit)",
"cmd.render_aging_describe": "Lão hóa bề mặt phân lớp",
"cmd.render_backends": "Kết xuất hình ảnh Sauends",
"cmd.render_batch": "Kết xuất mọi cảnh chụp",
"cmd.render_camera_fit": "Căn khung hình chủ thể kết xuất",
"cmd.render_camera_get": "Kiểm tra camera kết xuất",
"cmd.render_cancel": "Hủy bỏ Kết xuất hình ảnh",
"cmd.render_compare": "So sánh ảnh",
"cmd.render_effects_describe": "Hiệu ứng ống kính và sáng tạo",
"cmd.render_export": "Xuất hình ảnh kết xuất",
"cmd.render_hdr_inspect": "Kiểm tra tệp nguồn HDR",
@@ -897,6 +915,7 @@
"cmd.render_library_surface_prepare": "Chuẩn bị bản đồ bề mặt đã tạo",
"cmd.render_library_surface_unbind": "Xóa liên kết bề mặt",
"cmd.render_library_targets": "Tìm mục tiêu trong thư viện kết xuất",
"cmd.render_lights_solo": "Chỉ hiện một đèn",
"cmd.render_optics_bind": "Lưu liên kết quang học",
"cmd.render_optics_unbind": "Xóa liên kết quang học",
"cmd.render_preflight": "Kiểm tra yêu cầu kết xuất",
@@ -1006,7 +1025,9 @@
"cmd.sketch_spline_control_point": "Đường cong Spline qua điểm điều khiển",
"cmd.sketch_spline_control_point_5": "Đường cong Spline qua điểm điều khiển (Bậc 5)",
"cmd.sketch_spline_fit_point": "Đường cong Spline qua các điểm",
"cmd.sketch_spline_insert_point": "Thêm điểm vào đường cong Spline",
"cmd.sketch_spun_profile": "Biên dạng xoay tròn (Spun Profile)",
"cmd.sketch_toggle_diameter": "Chuyển đổi bán kính / đường kính",
"cmd.sketch_toggle_driven": "Chuyển đổi kích thước tham chiếu (Driven Dimension)",
"cmd.sketch_transform": "Biến đổi",
"cmd.sketch_trim_path": "Cắt xén dọc theo đường dẫn",
@@ -1072,6 +1093,7 @@
"cmd.view_rename": "Đổi tên góc nhìn đã lưu",
"cmd.view_right": "Góc nhìn bên phải",
"cmd.view_save": "Tạo góc nhìn đã lưu mới",
"cmd.view_shots_strip": "Dải cảnh chụp",
"cmd.view_spin": "Xoay quỹ đạo liên tục",
"cmd.view_tool": "Công cụ điều hướng",
"cmd.view_top": "Góc nhìn từ trên",
@@ -1145,6 +1167,7 @@
"document.save_changes": "Lưu các thay đổi đối với {name}?",
"document.save_copy": "Lưu bản sao…",
"document.unsaved_changes": "{name} có các thay đổi chưa lưu. Nếu không lưu, các thay đổi sẽ bị mất.",
"drive.rigid": "{joint} là khớp nối cứng: nó giữ cố định các chi tiết, nên không có gì để dẫn động. Để các chi tiết chuyển động, hãy đổi loại khớp nối trong “Chỉnh sửa khớp nối”: Revolute để xoay, Slider để trượt, Cylindrical cho cả hai.",
"drive.stopped_by_contact": "Đã dừng tại {value}: {a} sẽ xuyên qua {b}",
"drive.stopped_by_loop": "Đã dừng tại {value}: vòng lặp qua {joint} không thể khép kín thêm được nữa",
"drive.unconnected": "Không di chuyển gì: {part} không được nối với bất kỳ chi tiết cố định nào",
@@ -1286,6 +1309,9 @@
"install.steps_intro": "Safari cài đặt ứng dụng web từ menu của chính nó:",
"install.title": "Cài đặt SolidCraft",
"issue.body_label": "Nội dung báo cáo",
"issue.note_changed_while_waiting": "Cài đặt hoặc mô hình đã thay đổi trong lúc chờ bản xem trước. Hãy chụp lại màn hình để cập nhật chi tiết kỹ thuật cho góc nhìn hiện tại.",
"issue.note_details_kept": "Đã chụp ảnh màn hình trên máy. Chi tiết kỹ thuật bạn đã chỉnh sửa được giữ lại: hãy kiểm tra xem chúng còn khớp với góc nhìn hiện tại không.",
"issue.note_details_updated": "Chi tiết kỹ thuật đã được cập nhật theo góc nhìn hiện tại. Đã chụp ảnh màn hình trên máy. Hãy xem lại trước khi đính kèm trên GitHub.",
"issue.note_image_copied": "Hãy dán hình ảnh vào bản nháp GitHub, hoặc lưu tệp PNG nếu không thể sao chép hình ảnh.",
"issue.note_opened": "Hãy xem lại bản nháp GitHub và đính kèm ảnh chụp màn hình. Nút Submit new issue của GitHub sẽ đăng bản nháp.",
"issue.note_opened_copied": "Đã sao chép báo cáo. Hãy dán vào bản nháp GitHub, đính kèm ảnh chụp màn hình, rồi nhấp Submit new issue trên GitHub.",
@@ -1352,6 +1378,8 @@
"material.editor.unknown_card": "Chưa xác định đặc tính kỹ thuật. Có thể chỉ lưu khối lượng riêng đã gán và diện mạo hiện có.",
"material.editor.user_supplied": "Người dùng cung cấp",
"material.editor.wood": "Gỗ",
"material.selection.choose": "Chọn vật liệu",
"material.selection.mixed": "Các vật liệu khác nhau",
"measure.at": "Tại",
"measure.item": "Mục {number}",
"messages.ago_h": "{n} giờ trước",
@@ -1381,6 +1409,8 @@
"nav.free_orbit.help": "Chuột và chuột 3D xoay tự do qua cả hai cực và giữ nguyên độ lăn nghiêng (Roll). Bắt dính trục bằng phím Shift sẽ bị tắt.",
"nav.free_orbit.label": "Xoay góc nhìn tự do",
"nav.free_orbit.snap_help": "Chuột và chuột 3D xoay qua cả hai cực và giữ nguyên góc nghiêng cuộn. Giữ Shift trong khi kéo xoay quỹ đạo để bắt dính hướng nhìn và góc nghiêng cuộn.",
"nav.free_orbit_upright.help": "Kéo chuột sang ngang sẽ xoay mô hình quanh trục thẳng đứng của không gian; kéo lên và xuống sẽ nghiêng qua đỉnh mà không bị giới hạn.",
"nav.free_orbit_upright.label": "Xoay quanh trục thẳng đứng",
"nav.fusion.help": "Kéo chuột giữa để trượt, Shift + chuột giữa để xoay, kéo chuột phải để xoay",
"nav.inventor.help": "Kéo chuột giữa để trượt, Shift + chuột giữa để xoay, Ctrl + chuột giữa để thu phóng, kéo chuột phải để xoay",
"nav.openscad.help": "Kéo chuột trái trên vùng trống để xoay, Shift + chuột trái để quét chọn vùng, kéo hình học phác thảo để chỉnh sửa; kéo chuột phải hoặc chuột giữa để trượt, Shift + kéo chuột phải hoặc chuột giữa để thu phóng",
@@ -1456,6 +1486,10 @@
"prefs.design_note": "Cài đặt khởi đầu cho các tệp thiết kế mới. Các thiết kế đang mở giữ nguyên cài đặt riêng của chúng.",
"prefs.devices": "Chuột 3D",
"prefs.dialog_buttons": "Nút hộp thoại",
"prefs.dialogs": "Hộp thoại",
"prefs.dialogs_edge": "Ở cạnh khung nhìn",
"prefs.dialogs_left_column": "Trong cột bên trái",
"prefs.dialogs_note": "Nơi hộp thoại lệnh mở ra: gắn ở cạnh khung nhìn, hoặc trong cột bên trái thay cho trình duyệt và Lịch sử, hai phần này trở lại như cũ khi hộp thoại đóng.",
"prefs.display": "Hiển thị",
"prefs.edges": "Cạnh và đổ bóng",
"prefs.extend": "Kéo dài",
@@ -1515,6 +1549,8 @@
"prefs.opaque": "Đậm đặc (không trong suốt)",
"prefs.orbit_snap": "Giữ Shift khi xoay để bắt dính vào một trục tọa độ.",
"prefs.orientation": "Hướng nhìn",
"prefs.origin_plane_names": "Tên các mặt phẳng gốc",
"prefs.origin_plane_names_note": "Tên hiển thị trong trình duyệt, hộp thoại và danh sách. {top} là XY, {front} là XZ và {right} là YZ, với Z hướng lên. Lệnh, script và thiết kế đã lưu vẫn dùng XY, XZ và YZ.",
"prefs.overall_speed": "Tốc độ tổng thể",
"prefs.placement": "Vị trí đặt",
"prefs.prefer_free_sketch_points": "Ưu tiên điểm phác thảo có thể di chuyển khi các điểm chồng lên nhau",
@@ -1555,6 +1591,12 @@
"prefs.supersampling_brief": "Vẽ khung nhìn ở độ phân giải lớn hơn rồi lọc giảm tỷ lệ: rất sắc nét, nhưng tốn tài nguyên hơn.",
"prefs.tab_accent_note": "Màu nhấn theo tab đang kích hoạt",
"prefs.theme_file": "tệp chủ đề",
"prefs.theme_follow": "Theo dõi tệp chủ đề",
"prefs.theme_follow_error": "Chưa áp dụng: {error}. Chủ đề hợp lệ gần nhất vẫn được giữ.",
"prefs.theme_follow_launch": "Chỉ theo dõi trong lần chạy này (--theme-file); tải lại mỗi khi công cụ khác ghi lại tệp.",
"prefs.theme_follow_note": "Tải lại mỗi khi công cụ khác ghi lại tệp. Chọn chủ đề sẽ dừng theo dõi.",
"prefs.theme_follow_off": "Tắt",
"prefs.theme_follow_stop": "Dừng theo dõi",
"prefs.timeline_half_hour": "−30 phút",
"prefs.timeline_now": "bây giờ",
"prefs.timeline_quarter_hour": "−15 phút",
@@ -1619,6 +1661,7 @@
"render.aging_target_search": "Lọc tên khối",
"render.aging_upward": "Lắng đọng trên bề mặt hướng lên",
"render.all_controls_help": "Xem chi tiết toàn bộ các điều khiển hiện tại. Các trang chuyên mục cho phép chỉnh sửa tập trung mà không làm thay đổi yêu cầu cơ bản bên dưới.",
"render.all_shots_label": "Mọi cảnh chụp",
"render.appearance_physics_separate": "Diện mạo thị giác không gán khối lượng riêng, dữ liệu nhiệt hay các đặc tính kỹ thuật khác.",
"render.availability_native": "Tổ hợp gốc; hãy xem xét trước khi áp dụng",
"render.availability_projection": "Chiếu một phần được hỗ trợ; xem phạm vi và các mục lược bỏ",
@@ -1773,10 +1816,13 @@
"render.effects_reset_lens": "Đặt lại hiệu ứng ống kính",
"render.effects_seed": "Hạt giống hoa văn",
"render.export_display": "Xuất: ảnh PNG hiển thị",
"render.export_image_label": "Xuất ảnh",
"render.export_linear": "Xuất: ảnh EXR tuyến tính thô",
"render.exr_pending": "GPU có thể giữ lại tệp EXR tuyến tính thô. Tính năng nén, ảnh nhiều lớp và các kênh kết xuất phụ trợ vẫn chưa được triển khai.",
"render.film_grain": "Hạt nhiễu nhiếp ảnh (Film Grain)",
"render.fit": "Thu phóng vừa sản phẩm",
"render.fixed_camera": "Máy ảnh",
"render.fixed_world": "Thế giới",
"render.flare_ghosts": "Vòng bóng ma trên mỗi nguồn sáng",
"render.flare_halo": "Vầng hào quang lóe sáng",
"render.flare_streak": "Vệt sáng phong cách biến dạng (Anamorphic)",
@@ -1803,6 +1849,12 @@
"render.hdr_primaries_srgb": "Màu cơ bản: sRGB/D65",
"render.hdr_primaries_undeclared": "Tệp nguồn không khai báo màu cơ bản; hãy xem lại yêu cầu màu sắc của bộ kết xuất.",
"render.hdr_resource_missing": "Thiết kế này thiếu tài nguyên HDR đã lưu.",
"render.heading_batch": "LÔ",
"render.heading_frame": "KHUNG",
"render.heading_lens": "ỐNG KÍNH",
"render.heading_results": "KẾT QUẢ",
"render.heading_shots": "CẢNH CHỤP",
"render.hide_shots_label": "Ẩn dải cảnh chụp",
"render.image_advanced": "Điều khiển GPU nâng cao",
"render.image_cancelled": "Đã hủy kết xuất hình ảnh",
"render.image_cancelling": "Đang hủy…",
@@ -1834,6 +1886,7 @@
"render.image_width": "Bề rộng",
"render.include_rig_cards": "Bao gồm tấm hắt sáng và tấm chắn sáng thụ động",
"render.inspect_scene": "Làm mới tóm tắt cảnh đã ghi nhận",
"render.inspector": "Bảng thuộc tính",
"render.keep_editing": "Tiếp tục chỉnh sửa",
"render.layer_down": "Chuyển xuống",
"render.layer_enabled": "Bật lớp này",
@@ -1902,6 +1955,8 @@
"render.library_unfavorite": "Bỏ yêu thích",
"render.library_uniform_ack": "Sử dụng xấp xỉ đồng nhất không có hiệu ứng không gian hoặc nâng cao",
"render.library_usable": "Phép chiếu có thể thực thi",
"render.light_anchor_pending": "Chưa hỗ trợ đổi hệ tọa độ của đèn.",
"render.lights_fixed_to": "Đèn gắn cố định với",
"render.linear_help": "Giữ lại bản gốc RGBA số thực 32-bit trước khi khử nhiễu, phơi sáng, ánh xạ sắc điệu, lọc và phông nền. Tuyến tính sRGB/D65; nhân trước với độ phủ. Bộ nhớ bổ sung được tiếp nhận rõ ràng. Không nén hay thêm kênh phụ.",
"render.linear_retain": "Giữ lại tệp EXR tuyến tính thô",
"render.linear_unavailable": "Khung hình đã hoàn thành được chọn không giữ lại EXR. Hãy bật tính năng lưu giữ trước khi kết xuất; ảnh PNG cũ không thể khôi phục lại suất bức xạ thô.",
@@ -1987,6 +2042,7 @@
"render.output_over_budget": "Hình ảnh yêu cầu vượt quá giới hạn tài nguyên đã chọn. Hãy chủ động chọn kích thước hoặc cấu hình tương thích; không tự động hạ cấp chất lượng.",
"render.output_ratio_refused": "Kích thước liên kết vượt quá giới hạn của phương thức này. Kích thước cũ được giữ lại; hãy mở khóa tỷ lệ hoặc chọn giá trị nhỏ hơn.",
"render.output_swap_orientation": "Hoán đổi chiều rộng và chiều cao",
"render.output_tools_inspector": "Chỉnh kích thước và định dạng trong bảng thuộc tính Đầu ra.",
"render.page_aging": "Lão hóa và bám bẩn",
"render.page_all": "Tất cả điều khiển",
"render.page_appearance": "Diện mạo",
@@ -1998,6 +2054,7 @@
"render.page_lighting": "Chiếu sáng",
"render.page_output": "Đầu ra",
"render.page_scene": "Khung cảnh",
"render.paint_label": "Tô",
"render.passive_stage": "Mặt sàn và sân khấu thụ động",
"render.passive_stage_help": "Các tấm thụ động hữu hạn chỉ thuộc về hình ảnh kết xuất. Chúng không thêm khối CAD hay khối lượng. Vị trí được cố định sau khi áp dụng; giao cắt với ranh giới sản phẩm sẽ bị từ chối. CPU không cung cấp ánh sáng nảy khuếch tán.",
"render.perspective": "Camera phối cảnh",
@@ -2013,6 +2070,10 @@
"render.physical_prepare_plan": "Chuẩn bị kế hoạch tạo (không áp dụng)",
"render.physical_separate": "Diện mạo không phải là định danh vật liệu. Đây là các tham chiếu có nguồn gốc, không phải đặc tính hiệu dụng được chứng nhận. Khối lượng, vật liệu kỹ thuật và quang học hiện có giữ nguyên.",
"render.physical_unknown": "Chưa xác định ứng viên số học. Phải chỉ định mác, công thức hoặc cấu tạo; chưa rõ không có nghĩa là bằng 0.",
"render.pick_component": "Thành phần",
"render.pick_label": "Chọn",
"render.pick_up_label": "Lấy",
"render.pick_waiting": "Chưa hỗ trợ chọn sản phẩm.",
"render.post_contrast": "Độ tương phản",
"render.post_gain_b": "Khuếch đại xanh lam (Blue)",
"render.post_gain_g": "Khuếch đại xanh lục (Green)",
@@ -2025,6 +2086,7 @@
"render.preview": "Xem trước tích hợp",
"render.preview_help": "Đổ bóng raster và trộn độ trong suốt. Khúc xạ, HDRI và kết xuất cuối cùng hiện chưa khả dụng.",
"render.preview_image_settings_help": "Xem trước trực tiếp giữ nguyên ánh sáng studio cố định. Độ phơi sáng và ánh sáng hình ảnh sẽ áp dụng cho tác vụ khi bạn Kiểm tra hoặc Bắt đầu.",
"render.preview_lighting_help": "Bản xem trước trực tiếp chỉ hiện ánh sáng trực tiếp và không có bóng đổ.",
"render.preview_retry": "Thử lại xem trước trực tiếp",
"render.preview_unavailable_help": "Bản xem trước trực tiếp không khả dụng. Các thiết lập đã lưu và điều khiển tác vụ ảnh vẫn có thể truy cập. Hãy thay đổi cảnh hoặc thử lại; cảnh bị từ chối sẽ không được dựng lại trên mỗi khung hình.",
"render.preview_upload": "Dung lượng đỉnh xem trước trực tiếp",
@@ -2038,8 +2100,10 @@
"render.radial_distortion": "Độ méo hướng tâm",
"render.regrade_action": "Chỉnh màu lại bản gốc thô (không khử nhiễu)",
"render.regrade_help": "Tạo một ảnh PNG riêng biệt từ bản gốc thô đã hoàn thành được chọn, sử dụng các điều khiển trình chiếu này. Không dò lại tia, không đổi kích thước/camera/ánh sáng đã ghi nhận, và không ghi đè tệp EXR. Bộ khử nhiễu không áp dụng lại do bản gốc thô không giữ dữ liệu dẫn hướng. Bản phái sinh vẫn có thể xuất qua cùng điều khiển tác vụ.",
"render.regrade_label": "Chỉnh lại màu",
"render.reload_settings": "Tải lại cài đặt đã lưu",
"render.replace_ground": "Xác nhận thay thế sân khấu hiện có",
"render.replace_label": "Thay",
"render.replace_saved_surface": "Thay thế liên kết đã lưu hiện có cho mục tiêu này",
"render.restore_camera": "Khôi phục camera đã lưu",
"render.result_actual_help": "Một pixel hình ảnh tương ứng một pixel màn hình hiển thị.",
@@ -2085,6 +2149,7 @@
"render.sampling_title": "Lấy mẫu thích ứng và truyền dẫn",
"render.save_continue": "Lưu và tiếp tục",
"render.save_setup": "Lưu thiết lập",
"render.save_shot": "Lưu cảnh chụp",
"render.saved_maps_runtime_conflict": "Liên kết bề mặt đã lưu không thể kết hợp với ghi đè quang học tạm thời hoặc tệp HDR chưa lưu. Hãy lưu HDR hoặc chủ động xóa các đầu vào tạm thời.",
"render.saved_surface_remove": "Xóa liên kết đã lưu",
"render.saved_surfaces": "Liên kết bề mặt đã lưu",
@@ -2104,6 +2169,9 @@
"render.setup_runtime_unsupported": "Toàn bộ tác vụ này chưa thể lưu: nó chứa bản đồ hoặc liên kết khối trong cảnh. Hãy xóa các đầu vào tạm thời đó trước khi lưu thiết lập di động.",
"render.setup_save_missing": "Không tìm thấy cấu hình kết xuất đã lưu sau khi lệnh hoàn tất.",
"render.sharpening": "Làm sắc nét chi tiết",
"render.shell_tab_appearance": "DIỆN MẠO KẾT XUẤT",
"render.shell_tab_camera": "MÁY ẢNH KẾT XUẤT",
"render.shell_tab_scene": "CẢNH KẾT XUẤT",
"render.shot_duplicate": "Nhân bản khung hình đã lưu",
"render.shot_manager": "Khung hình đã lưu",
"render.shot_new_name": "Tên khung hình mới",
@@ -2112,11 +2180,19 @@
"render.shot_remove_title": "Xóa khung hình đã lưu?",
"render.shot_rename": "Đổi tên khung hình đã lưu",
"render.shot_saved_only": "Hãy lưu bản nháp hiện tại trước. Nhân bản và đổi tên sẽ giữ nguyên các tùy chọn đã lưu và tham chiếu tài nguyên chính xác.",
"render.show_shots_label": "Hiện dải cảnh chụp",
"render.sky_ground": "Mặt đất · RGB tuyến tính",
"render.sky_horizon": "Đường chân trời · RGB tuyến tính",
"render.sky_zenith": "Thiên đỉnh · RGB tuyến tính",
"render.soft_focus": "Lấy nét mềm (Soft focus)",
"render.stage_frame": "Khung",
"render.stage_light": "Ánh sáng",
"render.stage_look": "Giao diện",
"render.stage_orthographic": "Trực giao",
"render.stage_output": "Đầu ra",
"render.stage_panel_count": "Các tấm panel đã lưu",
"render.stage_preview_note": "Xem trước tích hợp · ánh sáng trực tiếp",
"render.strip_label": "Dải",
"render.studio_add_area": "Thêm nguồn sáng vùng",
"render.studio_ambient": "Suất bức xạ môi trường không đổi",
"render.studio_ambient_rgb": "Suất bức xạ môi trường không đổi (RGB tuyến tính)",
@@ -2173,6 +2249,12 @@
"render.volume_max": "Ranh giới tối đa · mm thế giới",
"render.volume_min": "Ranh giới tối thiểu · mm thế giới",
"render.volume_title": "Thể tích đồng nhất có giới hạn",
"render.wait_batch": "chưa hỗ trợ: hàng đợi kết xuất cảnh chụp đã lưu",
"render.wait_compare": "chưa hỗ trợ: so sánh ảnh được giữ lại",
"render.wait_paint": "chưa hỗ trợ: chọn sản phẩm và tô diện mạo",
"render.wait_pick_up": "chưa hỗ trợ: chọn sản phẩm và lấy mẫu diện mạo",
"render.wait_replace": "chưa hỗ trợ: thay mọi chỗ dùng một diện mạo",
"render.wait_solo": "chưa hỗ trợ: hiển thị và chỉ hiện một đèn",
"sample.bracket.about": "Mép chấn đáy với các mép chấn cạnh và đường viền gấp cuộn theo quy tắc 2 mm",
"sample.bracket.name": "Giá đỡ kim loại tấm",
"sample.enclosure.about": "Hộp làm mỏng vỏ có gờ chắn, vấu bắt vít, gờ đỡ và khớp gài bấm",
@@ -2264,6 +2346,7 @@
"timeline.delete_after_marker_one": "Xóa 1 tính năng sau điểm đánh dấu lịch sử",
"timeline.delete_after_marker_other": "Xóa {count} tính năng sau điểm đánh dấu lịch sử",
"timeline.delete_folders_tip": "Giữ lại các tính năng và thư mục lồng nhau trong lịch sử",
"timeline.delete_folders_with_features_tip": "Xóa các thư mục cùng mọi tính năng trong đó và các thư mục lồng bên trong",
"timeline.delete_many": "Xóa {count} tính năng đã chọn",
"timeline.feature_error": "Lỗi: {message}",
"timeline.feature_hint": "Bấm đúp để sửa · kéo rê để đổi thứ tự · bấm chuột phải để xem thêm",
@@ -2273,6 +2356,7 @@
"timeline.move_to_group": "Chuyển tính năng vào thư mục",
"timeline.rename_group": "Đổi tên thư mục tính năng",
"timeline.reorder_many": "Sắp xếp lại các tính năng",
"timeline.suppress_folders_tip": "Bỏ kích hoạt hoặc kích hoạt lại mọi tính năng trong các thư mục và các thư mục lồng bên trong",
"timeline.ungroup": "Xóa thư mục tính năng",
"to the face": "tới bề mặt",
"to the next face": "tới bề mặt tiếp theo",
@@ -2283,7 +2367,11 @@
"toolbar.render_camera": "CAMERA",
"toolbar.render_environment": "MÔI TRƯỜNG",
"toolbar.render_lighting": "CHIẾU SÁNG",
"toolbar.render_lights": "ĐÈN",
"toolbar.render_result": "KẾT QUẢ",
"toolbar.render_scene": "BỐI CẢNH",
"toolbar.render_stage": "GIAI ĐOẠN",
"toolbar.render_studio": "XƯỞNG",
"trace.points": "{count} điểm dọc theo đường dẫn",
"trace.stopped_by_contact": "Kết thúc tại mẫu {sample}: {a} sẽ xuyên qua {b}",
"trace.stopped_by_loop": "Kết thúc tại mẫu {sample}: vòng lặp qua {joint} không thể khép kín thêm được nữa",
@@ -2462,6 +2550,7 @@
"ui.agent_chat_import_failed": "{name}: nhập tệp thất bại: {error}",
"ui.agent_chat_intro": "Trò chuyện với tác tử lập trình (AI agent) bạn đang dùng hoặc một điểm cuối mô hình bạn chọn. AI thao tác trên thiết kế đang mở thông qua các công cụ của SolidCraft: các thay đổi hiển thị ngay khi thực hiện và có thể hoàn tác theo từng bước cho mỗi tin nhắn. Các thao tác AI được phép thực hiện mà không cần hỏi được đặt trong Tùy chọn cài đặt > AI.",
"ui.agent_chat_key_in_env": "đã đặt khóa",
"ui.agent_chat_last_missing": "Tác tử dùng gần nhất ({agent}) không còn khả dụng. Hãy chọn một tác tử rồi nhấn “Thử lại”.",
"ui.agent_chat_left": "Trái",
"ui.agent_chat_limit": "AI Agent đã đạt giới hạn {limit} thay đổi cho một tin nhắn. Hãy gửi tin nhắn tiếp theo để tiếp tục.",
"ui.agent_chat_login_found": "đã tìm thấy thông tin đăng nhập",
@@ -2508,6 +2597,7 @@
"ui.agent_chat_remove": "Xóa khỏi hàng đợi",
"ui.agent_chat_remove_file": "Bỏ tệp này khỏi tin nhắn",
"ui.agent_chat_remove_imported": "Gỡ khỏi tin nhắn này; chi tiết vẫn nằm trong thiết kế (sử dụng Hoàn tác để xóa)",
"ui.agent_chat_retry": "Thử lại",
"ui.agent_chat_right": "Phải (Hình chiếu cạnh)",
"ui.agent_chat_save": "Lưu",
"ui.agent_chat_send": "Gửi",
@@ -2520,6 +2610,7 @@
"ui.agent_chat_state_auth": "Cần đăng nhập",
"ui.agent_chat_state_busy": "Đang xử lý…",
"ui.agent_chat_state_exited": "Tác tử đã dừng",
"ui.agent_chat_state_failed": "Kết nối thất bại",
"ui.agent_chat_state_off": "Chưa kết nối",
"ui.agent_chat_state_ready": "Sẵn sàng",
"ui.agent_chat_state_starting": "Đang khởi động…",
@@ -2823,6 +2914,7 @@
"ui.fully_constrained": "Ràng buộc hoàn toàn",
"ui.function": "Hàm",
"ui.general": "Chung",
"ui.gpu_memory_lowered": "Card đồ họa đã hết bộ nhớ: khung nhìn được vẽ với chất lượng thấp hơn.",
"ui.graphics": "Đồ họa",
"ui.graphics_custom": "Tùy biến",
"ui.graphics_performance": "Hiệu năng",
@@ -3093,6 +3185,7 @@
"ui.render": "Kết xuất hình ảnh",
"ui.renderer": "Trình kết xuất hình ảnh (Renderer)",
"ui.repeat": "Lặp lại",
"ui.repeat_nothing": "Chưa có gì để lặp lại: hãy bắt đầu một công cụ hoặc hộp thoại trước.",
"ui.replace_with_link_done": "Các bản sao hiện đã trở thành các vị trí đặt của thiết kế liên kết.",
"ui.report_a_bug": "Báo cáo lỗi",
"ui.reset": "Đặt lại",
@@ -3315,5 +3408,6 @@
"web.open_link": "Mở thiết kế liên kết?",
"web.open_link_confirm": "Có những thay đổi chưa lưu. Mở thiết kế liên kết trong tab mới?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ chọn · Enter / Tab hoàn tất · Esc bỏ qua",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ để chọn · Enter để áp dụng nội dung đã nhập · Esc để đóng"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ để chọn · Enter để áp dụng nội dung đã nhập · Esc để đóng",
"⌘Enter continues on GitHub.": "Nhấn ⌘Enter để tiếp tục trên GitHub."
}
+6
View File
@@ -1003,6 +1003,12 @@
"pick one or two vertices, edges or faces": "選擇一個或兩個頂點、邊或面",
"prefs.cpu": "CPU(軟體)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "跟隨主題檔案",
"prefs.theme_follow_error": "未套用:{error}。保留上次有效的主題。",
"prefs.theme_follow_launch": "僅本次執行跟隨 (--theme-file);其他工具改寫檔案時會重新載入。",
"prefs.theme_follow_note": "其他工具改寫檔案時會重新載入。選擇主題將停止跟隨。",
"prefs.theme_follow_off": "關閉",
"prefs.theme_follow_stop": "停止跟隨",
"properties.title": "屬性:{name}",
"recovery.days_ago": "{count} 天前",
"recovery.detail": "{file} · {features} 個特徵 · 自動儲存於 {when}",
+95 -1
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "添加中间半径",
"Add Row": "添加行",
"Add a tool": "添加工具",
"Add all bodies": "添加所有实体",
"Add bend": "添加折弯",
"Add condition": "添加条件",
"Add or edit rule": "添加或编辑规则",
"Add to New Folder": "添加到新文件夹",
"Add visible bodies": "添加可见实体",
"Advanced": "高级",
"Against": "对照",
"Alignment": "对齐",
@@ -56,6 +58,7 @@
"BOUNDARY SURFACE": "边界曲面",
"BROWSER": "浏览器",
"Bar": "扁钢",
"Base Center": "底面中心",
"Bend": "折弯角度",
"Bend Edges": "折弯边",
"Bend Line Position": "弯折线位置",
@@ -140,6 +143,7 @@
"Create Issue — review draft": "创建 Issue——审阅草稿",
"Create Issue…": "创建 Issue…",
"Cross curves": "交叉曲线",
"Ctrl+Enter continues on GitHub.": "按Ctrl+Enter在GitHub中继续。",
"Cube": "立方体",
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
"Current perspective view": "当前透视视图",
@@ -152,6 +156,7 @@
"Customize…": "自定义…",
"Cylindrical face": "圆柱面",
"Delete": "删除",
"Delete Folders and Features…": "删除文件夹及其特征…",
"Delete Selected Features…": "删除所选特征…",
"Delete Selected Folders": "删除所选文件夹",
"Delete all": "全部删除",
@@ -430,6 +435,7 @@
"Remove this point": "移除此点",
"Rename Feature": "重命名特征",
"Rename Folder": "重命名文件夹",
"Render Stage": "渲染阶段",
"Reopen designs": "重新打开设计",
"Reopen the designs that were open when SolidCraft was last closed": "重新打开上次关闭 SolidCraft 时打开的设计",
"Replace existing library file": "替换现有库文件",
@@ -534,6 +540,7 @@
"Style": "样式",
"Summary": "摘要",
"Suppress": "抑制",
"Suppress Selected Folders": "抑制所选文件夹",
"Suppressed": "已抑制",
"Surface": "曲面",
"Surface, or faces to turn over": "曲面,或要翻转的面",
@@ -581,6 +588,7 @@
"UNTRIM": "取消修剪",
"Unit": "单位",
"Unsuppress": "取消抑制",
"Unsuppress Selected Folders": "取消抑制所选文件夹",
"Updating the preview": "正在更新预览",
"Use active author placement": "使用活动作者放置",
"Use fixed plane": "使用固定平面",
@@ -628,6 +636,8 @@
"ai.prefs.background_commands": "在后台执行代理命令",
"ai.prefs.connect_apps": "让此电脑上的 AI 应用可以使用 SolidCraft",
"ai.prefs.connect_apps_note": "SolidCraft 启动时,会把自己作为 MCP 服务器添加到此电脑上找到的 AI 应用(OpenCode、Claude Desktop、Claude Code、Cursor、Codex、Gemini CLI、Copilot CLI、Windsurf、Zed、VS Code)。只添加或更新 solidcraft 条目;你修改过的条目不会被改动,文件会先备份。关闭后,会删除它添加的条目。",
"ai.prefs.connect_at_start": "SolidCraft 启动时连接上次使用的智能体",
"ai.prefs.connect_at_start_note": "每次启动 SolidCraft 时,在后台启动上次连接的智能体(或默认智能体),以便随时开始聊天。使用 --control 启动 SolidCraft 时不会这样做。如果无法连接,助手会显示“连接失败”和“重试”按钮。",
"ai.prefs.follow_camera": "将相机转向草图",
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
@@ -635,6 +645,8 @@
"ai.prefs.make_default": "设为默认",
"ai.prefs.no_agents": "尚无代理。添加代理或刷新以查找已安装的代理。",
"ai.prefs.no_saved_approvals": "没有已保存的批准。",
"ai.prefs.open_at_start": "SolidCraft 启动时打开助手面板",
"ai.prefs.open_at_start_note": "每次启动 SolidCraft 时显示助手面板(或其独立窗口)。使用 --control 启动 SolidCraft 时不会这样做。",
"ai.prefs.override_all_warning": "此代理可在不询问的情况下保存、覆盖、导出或替换设计。",
"ai.prefs.permission_all": "全部允许",
"ai.prefs.permission_all_summary": "可用的 SolidCraft 工具无需询问即可运行。",
@@ -679,6 +691,9 @@
"cmd.appearance_assign": "外观",
"cmd.appearance_library": "外观库",
"cmd.appearance_list": "列出外观",
"cmd.appearance_paint": "涂抹外观",
"cmd.appearance_pick_up": "拾取外观",
"cmd.appearance_replace": "替换外观",
"cmd.assembly_bom": "物料清单",
"cmd.body_rename": "重命名实体",
"cmd.browser_collapse_all": "全部折叠",
@@ -766,6 +781,7 @@
"cmd.document_revision": "设计版本号",
"cmd.document_units": "更改设计单位",
"cmd.edit_checkpoint": "检查点",
"cmd.edit_repeat": "重复上一个命令",
"cmd.edit_restore_checkpoint": "恢复检查点",
"cmd.engine_commands": "列出命令",
"cmd.engine_describe": "描述命令",
@@ -879,9 +895,11 @@
"cmd.plastic_snap_fit": "卡扣",
"cmd.render_aging_describe": "分层表面老化",
"cmd.render_backends": "渲染后端",
"cmd.render_batch": "渲染所有镜头",
"cmd.render_camera_fit": "为渲染主体取景",
"cmd.render_camera_get": "检查渲染相机",
"cmd.render_cancel": "取消渲染",
"cmd.render_compare": "比较图像",
"cmd.render_effects_describe": "镜头与创意效果",
"cmd.render_export": "导出 PNG",
"cmd.render_hdr_inspect": "检查 HDR 源",
@@ -897,6 +915,7 @@
"cmd.render_library_surface_prepare": "准备已制作的表面贴图",
"cmd.render_library_surface_unbind": "移除表面绑定",
"cmd.render_library_targets": "查找渲染库目标",
"cmd.render_lights_solo": "单独显示灯光",
"cmd.render_optics_bind": "保存光学绑定",
"cmd.render_optics_unbind": "移除光学绑定",
"cmd.render_preflight": "检查渲染请求",
@@ -1006,7 +1025,9 @@
"cmd.sketch_spline_control_point": "控制点样条曲线",
"cmd.sketch_spline_control_point_5": "控制点样条曲线(5 次)",
"cmd.sketch_spline_fit_point": "拟合点样条曲线",
"cmd.sketch_spline_insert_point": "添加样条曲线点",
"cmd.sketch_spun_profile": "旋转轮廓",
"cmd.sketch_toggle_diameter": "切换半径 / 直径",
"cmd.sketch_toggle_driven": "切换被驱动尺寸",
"cmd.sketch_transform": "变换",
"cmd.sketch_trim_path": "沿路径修剪",
@@ -1072,6 +1093,7 @@
"cmd.view_rename": "重命名命名视图",
"cmd.view_right": "右视图",
"cmd.view_save": "新建命名视图",
"cmd.view_shots_strip": "镜头条",
"cmd.view_spin": "连续环绕",
"cmd.view_tool": "导航工具",
"cmd.view_top": "俯视图",
@@ -1145,6 +1167,7 @@
"document.save_changes": "保存对 {name} 的更改?",
"document.save_copy": "保存副本…",
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
"drive.rigid": "{joint} 是刚性接头:它把零件固定在原位,因此没有可驱动的运动。要让零件运动,请在“编辑接头”中更改其类型:旋转用 Revolute,滑动用 Slider,两者兼有用 Cylindrical。",
"drive.stopped_by_contact": "停在 {value}:{a} 会穿过 {b}",
"drive.stopped_by_loop": "停在 {value}:经过 {joint} 的环路无法再闭合",
"drive.unconnected": "没有任何移动:{part} 未连接到任何固定的零部件",
@@ -1286,6 +1309,9 @@
"install.steps_intro": "Safari 通过自身的菜单安装网页应用:",
"install.title": "安装 SolidCraft",
"issue.body_label": "报告正文",
"issue.note_changed_while_waiting": "等待预览时设置或模型已更改。重新截图即可按当前视图更新技术详情。",
"issue.note_details_kept": "已在本地截图。已保留你编辑过的技术详情:请检查它们是否仍与当前视图一致。",
"issue.note_details_updated": "技术详情已按当前视图更新。已在本地截图。附加到 GitHub 之前请先检查。",
"issue.note_image_copied": "请将图片粘贴到 GitHub 草稿中。如果无法复制图片,请保存 PNG。",
"issue.note_opened": "请检查 GitHub 草稿并附上截图。点击 GitHub 的 Submit new issue 按钮即可发布。",
"issue.note_opened_copied": "报告已复制。请粘贴到 GitHub 草稿中,附上截图,然后在 GitHub 上点击 Submit new issue。",
@@ -1352,6 +1378,8 @@
"material.editor.unknown_card": "工程属性未知。可以只保存指定密度和现有外观。",
"material.editor.user_supplied": "用户提供",
"material.editor.wood": "木材",
"material.selection.choose": "选择材料",
"material.selection.mixed": "不同材料",
"measure.at": "位置",
"measure.item": "项目 {number}",
"messages.ago_h": "{n} 小时前",
@@ -1381,6 +1409,8 @@
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
"nav.free_orbit.label": "自由旋转",
"nav.free_orbit.snap_help": "鼠标和3D鼠标可越过两极旋转,并保留画面旋转角度。在拖动环绕时按住Shift,可吸附视图方向和画面旋转角度。",
"nav.free_orbit_upright.help": "左右拖动鼠标时模型绕世界竖直轴旋转;上下拖动可不受限制地越过顶部倾斜。",
"nav.free_orbit_upright.label": "绕竖直轴旋转",
"nav.fusion.help": "中键拖动平移,Shift+中键拖动旋转,右键拖动旋转",
"nav.inventor.help": "中键拖动平移,Shift+中键拖动旋转,Ctrl+中键拖动缩放,右键拖动旋转",
"nav.openscad.help": "在空白处左键拖动旋转,Shift+左键拖动框选,拖动草图几何可编辑;右键或中键拖动平移,Shift+右键或 Shift+中键拖动缩放",
@@ -1456,6 +1486,10 @@
"prefs.design_note": "新建设计的初始设置。已打开的设计保留各自的设置。",
"prefs.devices": "3D 鼠标",
"prefs.dialog_buttons": "对话框按钮",
"prefs.dialogs": "对话框",
"prefs.dialogs_edge": "视图边缘",
"prefs.dialogs_left_column": "左侧栏",
"prefs.dialogs_note": "命令对话框的打开位置:停靠在视图边缘,或在左侧栏中替代浏览器和历史;关闭对话框后,浏览器和历史恢复原样。",
"prefs.display": "显示",
"prefs.edges": "边缘与着色",
"prefs.extend": "扩展",
@@ -1515,6 +1549,8 @@
"prefs.opaque": "不透明",
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
"prefs.orientation": "方向",
"prefs.origin_plane_names": "原点平面名称",
"prefs.origin_plane_names_note": "浏览器、对话框和列表中显示的名称。{top} 即 XY,{front} 即 XZ,{right} 即 YZ(Z 轴朝上)。命令、脚本和已保存的设计仍使用 XY、XZ 和 YZ。",
"prefs.overall_speed": "整体速度",
"prefs.placement": "放置",
"prefs.prefer_free_sketch_points": "草图点重合时优先选择可移动的点",
@@ -1555,6 +1591,12 @@
"prefs.supersampling_brief": "以更高分辨率绘制后滤波缩小:更清晰,但负载更高。",
"prefs.tab_accent_note": "强调色跟随当前选项卡",
"prefs.theme_file": "主题文件",
"prefs.theme_follow": "跟随主题文件",
"prefs.theme_follow_error": "未应用:{error}。保留上次有效的主题。",
"prefs.theme_follow_launch": "仅本次运行跟随 (--theme-file);其他工具改写文件时会重新加载。",
"prefs.theme_follow_note": "其他工具改写文件时会重新加载。选择主题将停止跟随。",
"prefs.theme_follow_off": "关闭",
"prefs.theme_follow_stop": "停止跟随",
"prefs.timeline_half_hour": "−30 分钟",
"prefs.timeline_now": "现在",
"prefs.timeline_quarter_hour": "−15",
@@ -1619,6 +1661,7 @@
"render.aging_target_search": "筛选实体名称",
"render.aging_upward": "向上表面沉积",
"render.all_controls_help": "集中显示当前所有控件;各分页可针对相应任务编辑,不改变底层请求。",
"render.all_shots_label": "所有镜头",
"render.appearance_physics_separate": "视觉外观不会分配密度、热学数据或其他工程属性。",
"render.availability_native": "原生组合;应用前请审阅",
"render.availability_projection": "支持的部分映射;请查看范围和省略项",
@@ -1773,10 +1816,13 @@
"render.effects_reset_lens": "重置镜头效果",
"render.effects_seed": "图案种子",
"render.export_display": "导出:显示 PNG",
"render.export_image_label": "导出图像",
"render.export_linear": "导出:原始线性 EXR",
"render.exr_pending": "GPU 可保留线性 EXR;压缩、多层和辅助通道仍未实现。",
"render.film_grain": "摄影颗粒",
"render.fit": "适配产品视图",
"render.fixed_camera": "相机",
"render.fixed_world": "世界",
"render.flare_ghosts": "每光源鬼影环数",
"render.flare_halo": "耀斑光晕",
"render.flare_streak": "变形镜头风格光条",
@@ -1803,6 +1849,12 @@
"render.hdr_primaries_srgb": "颜色原色:sRGB/D65",
"render.hdr_primaries_undeclared": "源文件未声明原色;请查看渲染器的颜色要求。",
"render.hdr_resource_missing": "此设计缺少已保存的HDR资源。",
"render.heading_batch": "批量",
"render.heading_frame": "取景框",
"render.heading_lens": "镜头设置",
"render.heading_results": "结果",
"render.heading_shots": "镜头",
"render.hide_shots_label": "隐藏镜头条",
"render.image_advanced": "GPU 高级设置",
"render.image_cancelled": "渲染已取消",
"render.image_cancelling": "正在取消",
@@ -1834,6 +1886,7 @@
"render.image_width": "宽度",
"render.include_rig_cards": "包含被动反光板和遮光板",
"render.inspect_scene": "刷新捕获的场景摘要",
"render.inspector": "检查器",
"render.keep_editing": "继续编辑",
"render.layer_down": "下移",
"render.layer_enabled": "启用此层",
@@ -1902,6 +1955,8 @@
"render.library_unfavorite": "移除收藏",
"render.library_uniform_ack": "使用不含空间或高级效果的均匀近似",
"render.library_usable": "可执行映射",
"render.light_anchor_pending": "暂不支持更改灯光坐标系。",
"render.lights_fixed_to": "灯光固定到",
"render.linear_help": "保留降噪、曝光、色调映射、滤镜和背景处理前的 32 位浮点 RGBA。线性 sRGB/D65,预乘覆盖率。额外内存经明确准入检查;无压缩或额外通道。",
"render.linear_retain": "保留原始线性 EXR",
"render.linear_unavailable": "所选已完成图像没有保留 EXR。请在渲染前启用保留;旧 PNG 无法恢复原始辐亮度。",
@@ -1987,6 +2042,7 @@
"render.output_over_budget": "请求图像超出所选资源范围;请明确选择兼容尺寸或配置,不会自动降级。",
"render.output_ratio_refused": "关联尺寸超出此方式的上限;保留原尺寸。请解锁宽高比或选择较小值。",
"render.output_swap_orientation": "交换宽度和高度",
"render.output_tools_inspector": "请在输出检查器中编辑尺寸和格式。",
"render.page_aging": "老化与沉积",
"render.page_all": "全部控件",
"render.page_appearance": "外观",
@@ -1998,6 +2054,7 @@
"render.page_lighting": "照明",
"render.page_output": "输出",
"render.page_scene": "场景",
"render.paint_label": "涂抹",
"render.passive_stage": "地面和被动舞台",
"render.passive_stage_help": "有限的被动面板仅属于渲染图像,不增加 CAD 实体或质量。应用后位置固定,与主体边界相交时会拒绝。CPU 不提供漫反射间接照明。",
"render.perspective": "透视相机",
@@ -2013,6 +2070,10 @@
"render.physical_prepare_plan": "准备创建计划(不应用)",
"render.physical_separate": "外观不等于材料识别。这些是有来源的参考,不是经认证的等效属性。现有质量、工程材料和光学设置保持不变。",
"render.physical_unknown": "尚未确定数值候选资料。必须指定牌号、配方或构造;未知不等于零。",
"render.pick_component": "组件",
"render.pick_label": "拾取",
"render.pick_up_label": "拾取",
"render.pick_waiting": "暂不支持产品拾取。",
"render.post_contrast": "对比度",
"render.post_gain_b": "蓝色增益",
"render.post_gain_g": "绿色增益",
@@ -2025,6 +2086,7 @@
"render.preview": "集成预览",
"render.preview_help": "光栅着色和混合透明度。暂不支持折射、HDRI 和最终渲染。",
"render.preview_image_settings_help": "实时预览使用固定工作室照明。曝光和图像照明在检查或开始时应用于图像任务。",
"render.preview_lighting_help": "实时预览仅显示直接光照,不显示阴影。",
"render.preview_retry": "重试实时预览",
"render.preview_unavailable_help": "实时预览不可用,但已保存设置和图像任务控件仍可访问。请修改场景或明确重试;被拒绝的场景不会每帧重建。",
"render.preview_upload": "实时预览顶点数据量",
@@ -2038,8 +2100,10 @@
"render.radial_distortion": "径向畸变",
"render.regrade_action": "重新调整原始母版(不降噪)",
"render.regrade_help": "根据所选已完成原始母版和这些显示设置创建独立 PNG。不追踪光线,不改变已捕获的尺寸、相机或灯光,也不覆盖 EXR。原始母版不保留降噪辅助数据,因此不重新降噪。可通过相同任务控件导出派生结果。",
"render.regrade_label": "重新调色",
"render.reload_settings": "重新加载已保存设置",
"render.replace_ground": "确认替换现有舞台",
"render.replace_label": "替换",
"render.replace_saved_surface": "替换此目标已有的保存绑定",
"render.restore_camera": "恢复已保存相机",
"render.result_actual_help": "一个图像像素对应一个屏幕像素。",
@@ -2085,6 +2149,7 @@
"render.sampling_title": "自适应与透射采样",
"render.save_continue": "保存并继续",
"render.save_setup": "保存设置",
"render.save_shot": "保存镜头",
"render.saved_maps_runtime_conflict": "已保存表面绑定不能与临时光学覆盖或未保存 HDR 混用。请明确保存 HDR 或移除临时输入。",
"render.saved_surface_remove": "移除已保存绑定",
"render.saved_surfaces": "已保存表面绑定",
@@ -2104,6 +2169,9 @@
"render.setup_runtime_unsupported": "此完整任务暂时无法保存:其中包含贴图或场景实体绑定。保存可移植设置前,请移除这些临时输入。",
"render.setup_save_missing": "命令完成后未找到保存的渲染设置。",
"render.sharpening": "细节锐化",
"render.shell_tab_appearance": "外观",
"render.shell_tab_camera": "相机",
"render.shell_tab_scene": "场景",
"render.shot_duplicate": "复制已保存镜头",
"render.shot_manager": "已保存镜头",
"render.shot_new_name": "新镜头名称",
@@ -2112,11 +2180,19 @@
"render.shot_remove_title": "删除已保存镜头?",
"render.shot_rename": "重命名已保存镜头",
"render.shot_saved_only": "请先保存当前草稿;复制和重命名会保留精确的保存选项与资源引用。",
"render.show_shots_label": "显示镜头条",
"render.sky_ground": "地面 · 线性 RGB",
"render.sky_horizon": "地平线 · 线性 RGB",
"render.sky_zenith": "天顶 · 线性 RGB",
"render.soft_focus": "柔焦",
"render.stage_frame": "构图",
"render.stage_light": "照明",
"render.stage_look": "外观",
"render.stage_orthographic": "正交投影",
"render.stage_output": "输出",
"render.stage_panel_count": "已保存面板",
"render.stage_preview_note": "集成预览 · 直接光照",
"render.strip_label": "镜头条",
"render.studio_add_area": "添加面光源",
"render.studio_ambient": "恒定环境辐亮度",
"render.studio_ambient_rgb": "恒定环境辐亮度(线性RGB)",
@@ -2173,6 +2249,12 @@
"render.volume_max": "最大边界 · 世界坐标 mm",
"render.volume_min": "最小边界 · 世界坐标 mm",
"render.volume_title": "有界均匀体积",
"render.wait_batch": "暂不支持:已保存镜头渲染队列",
"render.wait_compare": "暂不支持:保留图像比较",
"render.wait_paint": "暂不支持:产品拾取和外观涂绘",
"render.wait_pick_up": "暂不支持:产品拾取和外观采样",
"render.wait_replace": "暂不支持:替换外观的所有使用处",
"render.wait_solo": "暂不支持:灯光可见性和独显控制",
"sample.bracket.about": "基于 2 mm 钣金规则的基础法兰、边线法兰和卷边",
"sample.bracket.name": "钣金支架",
"sample.enclosure.about": "带凸缘、螺钉凸台、垫块和卡扣的抽壳长方体",
@@ -2264,6 +2346,7 @@
"timeline.delete_after_marker_one": "删除历史标记之后的 1 个特征",
"timeline.delete_after_marker_other": "删除历史标记之后的 {count} 个特征",
"timeline.delete_folders_tip": "在历史记录中保留其中的特征和嵌套文件夹",
"timeline.delete_folders_with_features_tip": "删除文件夹及其中的所有特征和嵌套文件夹",
"timeline.delete_many": "删除所选的 {count} 个特征",
"timeline.feature_error": "错误:{message}",
"timeline.feature_hint": "双击编辑 · 拖动以重新排序 · 右键查看更多操作",
@@ -2273,6 +2356,7 @@
"timeline.move_to_group": "将特征移至文件夹",
"timeline.rename_group": "重命名特征文件夹",
"timeline.reorder_many": "特征重新排序",
"timeline.suppress_folders_tip": "抑制或取消抑制文件夹及其嵌套文件夹中的所有特征",
"timeline.ungroup": "移除特征文件夹",
"to the face": "到面",
"to the next face": "到下一个面",
@@ -2283,7 +2367,11 @@
"toolbar.render_camera": "相机",
"toolbar.render_environment": "环境",
"toolbar.render_lighting": "照明",
"toolbar.render_lights": "灯光",
"toolbar.render_result": "结果",
"toolbar.render_scene": "场景",
"toolbar.render_stage": "舞台",
"toolbar.render_studio": "工作室",
"trace.points": "路径上的 {count} 个点",
"trace.stopped_by_contact": "在采样 {sample} 处结束:{a} 会穿过 {b}",
"trace.stopped_by_loop": "在采样 {sample} 处结束:经过 {joint} 的环路无法再闭合",
@@ -2462,6 +2550,7 @@
"ui.agent_chat_import_failed": "{name}:导入失败:{error}",
"ui.agent_chat_intro": "与你已在使用的编程智能体或你选择的模型端点聊天。它通过 SolidCraft 的工具处理打开的设计:更改即时显示,每条消息的更改可一步撤销。无需询问即可执行的范围在“首选项 > AI”中设置。",
"ui.agent_chat_key_in_env": "已设置密钥",
"ui.agent_chat_last_missing": "上次使用的智能体({agent})已不可用。请选择一个智能体,然后按“重试”。",
"ui.agent_chat_left": "左侧",
"ui.agent_chat_limit": "智能体已达到每条消息 {limit} 项更改的上限。再发送一条消息可让它继续。",
"ui.agent_chat_login_found": "已找到登录信息",
@@ -2508,6 +2597,7 @@
"ui.agent_chat_remove": "从队列中移除",
"ui.agent_chat_remove_file": "从这条消息中移除",
"ui.agent_chat_remove_imported": "从这条消息中移除;零件仍保留在设计中(撤销可将其删除)",
"ui.agent_chat_retry": "重试",
"ui.agent_chat_right": "右侧",
"ui.agent_chat_save": "保存",
"ui.agent_chat_send": "发送",
@@ -2520,6 +2610,7 @@
"ui.agent_chat_state_auth": "需要登录",
"ui.agent_chat_state_busy": "工作中…",
"ui.agent_chat_state_exited": "智能体已停止",
"ui.agent_chat_state_failed": "连接失败",
"ui.agent_chat_state_off": "未连接",
"ui.agent_chat_state_ready": "就绪",
"ui.agent_chat_state_starting": "正在启动…",
@@ -2823,6 +2914,7 @@
"ui.fully_constrained": "完全约束",
"ui.function": "函数",
"ui.general": "常规",
"ui.gpu_memory_lowered": "显卡内存不足:视图以较低画质绘制。",
"ui.graphics": "图形",
"ui.graphics_custom": "自定义",
"ui.graphics_performance": "性能",
@@ -3093,6 +3185,7 @@
"ui.render": "渲染",
"ui.renderer": "渲染器",
"ui.repeat": "重复",
"ui.repeat_nothing": "还没有可重复的操作:请先启动一个工具或对话框。",
"ui.replace_with_link_done": "这些副本现在是链接设计的放置。",
"ui.report_a_bug": "报告缺陷",
"ui.reset": "重置",
@@ -3315,5 +3408,6 @@
"web.open_link": "打开链接的设计?",
"web.open_link_confirm": "有未保存的更改。要在新标签页中打开链接的设计吗?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 选择 · Enter / Tab 补全 · Esc 关闭",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 选择 · Enter 按原样应用 · Esc 关闭"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 选择 · Enter 按原样应用 · Esc 关闭",
"⌘Enter continues on GitHub.": "按⌘Enter在GitHub中继续。"
}
@@ -5,12 +5,12 @@ GenericName=3D CAD
Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild
Exec=solidcraft %f
TryExec=solidcraft
Icon=ai.storyteller.solidcraft
Icon=io.tangencylabs.solidcraft
Terminal=false
StartupNotify=true
StartupWMClass=ai.storyteller.solidcraft
StartupWMClass=io.tangencylabs.solidcraft
Categories=Graphics;3DGraphics;Engineering;
Keywords=cad;3d;parametric;sketch;step;stl;modelling;modeling;
MimeType=application/x-solidcraft;model/step;model/iges;model/stl;model/3mf;
# Releases before 0.8 used this desktop id; launchers keep pinned entries across the rename.
X-Flatpak-RenamedFrom=ai.storyteller.solvecraft.desktop;
# Desktop ids of earlier releases; launchers keep pinned entries across the renames.
X-Flatpak-RenamedFrom=ai.storyteller.solidcraft.desktop;ai.storyteller.solvecraft.desktop;
@@ -2,7 +2,7 @@
<!-- AppStream metadata (software centres, Flathub). Template: package.sh substitutes @VERSION@
and @DATE@ from Cargo.toml and the build date. -->
<component type="desktop-application">
<id>ai.storyteller.solidcraft</id>
<id>io.tangencylabs.solidcraft</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>SolidCraft</name>
@@ -24,8 +24,8 @@
<li>Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server</li>
</ul>
</description>
<launchable type="desktop-id">ai.storyteller.solidcraft.desktop</launchable>
<icon type="stock">ai.storyteller.solidcraft</icon>
<launchable type="desktop-id">io.tangencylabs.solidcraft.desktop</launchable>
<icon type="stock">io.tangencylabs.solidcraft</icon>
<url type="homepage">https://docs.solidcraft.io/</url>
<url type="bugtracker">https://github.com/bherbruck/solidcraft/issues</url>
<url type="help">https://docs.solidcraft.io/</url>
@@ -56,11 +56,15 @@
<binary>solidcraft</binary>
<binary>solidcraft-cli</binary>
<mediatype>application/x-solidcraft</mediatype>
<!-- The desktop id of releases before 0.8 (docs/compatibility.md). -->
<!-- The app and desktop ids of earlier releases (docs/compatibility.md). -->
<id>ai.storyteller.solidcraft</id>
<id>ai.storyteller.solidcraft.desktop</id>
<id>ai.storyteller.solvecraft.desktop</id>
</provides>
<!-- Releases before 0.8 used the app id ai.storyteller.solvecraft (docs/compatibility.md). -->
<!-- Earlier app ids (docs/compatibility.md): ai.storyteller.solidcraft from 0.8 until the move
to Tangency Labs, ai.storyteller.solvecraft before 0.8. -->
<replaces>
<id>ai.storyteller.solidcraft</id>
<id>ai.storyteller.solvecraft</id>
</replaces>
<content_rating type="oars-1.1"/>
@@ -4,7 +4,7 @@
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-solidcraft">
<comment>SolidCraft design</comment>
<icon name="ai.storyteller.solidcraft"/>
<icon name="io.tangencylabs.solidcraft"/>
<sub-class-of type="application/json"/>
<glob pattern="*.solidcraft"/>
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename. -->
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleExecutable</key>
<string>SolidCraft</string>
<key>CFBundleIdentifier</key>
<string>ai.storyteller.solidcraft</string>
<string>io.tangencylabs.solidcraft</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
+2
View File
@@ -73,6 +73,7 @@ impl Tokens {
form_ink: Color32::from_rgb(27, 36, 48),
form_dim: Color32::from_rgb(74, 88, 104),
form_primary: Color32::from_rgb(35, 110, 173),
form_primary_fill: Color32::from_rgb(35, 110, 173),
form_error: Color32::from_rgb(161, 52, 44),
form_error_fill: Color32::from_rgb(252, 239, 237),
form_success: Color32::from_rgb(39, 107, 44),
@@ -363,6 +364,7 @@ impl Tokens {
t.form_ink = t.text;
t.form_dim = Color32::from_rgb(180, 187, 200);
t.form_primary = t.accent;
t.form_primary_fill = t.form_primary;
t.form_error = t.error;
t.form_error_fill = t.timeline_item_error;
t.form_success = Color32::from_rgb(143, 219, 154);
+247
View File
@@ -0,0 +1,247 @@
{
"base": "dark",
"colors": {
"accent": "#ffffff",
"accent_soft": "#000000",
"agent_busy": "#ffffff",
"agent_ok": "#ffffff",
"app_bar": "#000000",
"app_bar_hover": "#000000",
"app_bar_hover_button": "#000000",
"app_bar_hover_strong": "#000000",
"app_bar_hover_subtle": "#000000",
"app_bar_separator": "#ffffff",
"app_bar_tab_active": "#000000",
"app_bar_text": "#ffffff",
"axis_x": "#ffffff",
"axis_y": "#ffffff",
"axis_z": "#ffffff",
"bend_down": "#ffffff",
"bend_up": "#ffffff",
"body": "#000000",
"body_edge": "#ffffff",
"body_selected": "#ffffff",
"border": "#ffffff",
"box_crossing": "#ffffff",
"box_window": "#ffffff",
"chip": "#ffffff",
"construction_geom": "#ffffff",
"construction_plane": "#000000",
"continuity": [
"#ffffff",
"#ffffff",
"#ffffff",
"#ffffff"
],
"control_border": "#ffffff",
"control_fill": "#000000",
"cube_border": "#ffffff",
"cube_face": "#000000",
"cube_hover": "#000000",
"cube_text": "#ffffff",
"dialog_bg": "#000000",
"dialog_body": "#000000",
"dialog_border": "#ffffff",
"dialog_footer": "#000000",
"dialog_label": "#ffffff",
"dialog_rule": "#ffffff",
"dimension": "#ffffff",
"dimension_driven": "#ffffff",
"error": "#ffffff",
"field": "#000000",
"file_background": "#000000",
"file_border": "#ffffff",
"file_dim": "#ffffff",
"file_image_tint": "#ffffff",
"file_primary": "#000000",
"file_primary_border": "#ffffff",
"file_primary_ink": "#ffffff",
"file_rail": "#000000",
"file_rail_hover": "#000000",
"file_rail_ink": "#ffffff",
"file_recovery": "#000000",
"file_recovery_border": "#ffffff",
"file_recovery_ink": "#ffffff",
"file_recovery_well": "#000000",
"file_surface": "#000000",
"form_accent": "#ffffff",
"form_dim": "#ffffff",
"form_error": "#ffffff",
"form_error_fill": "#000000",
"form_fill": "#ffffff",
"form_ink": "#ffffff",
"form_primary": "#000000",
"form_primary_fill": "#ffffff",
"form_soft": "#000000",
"form_success": "#ffffff",
"form_success_fill": "#000000",
"form_tab_ink": "#ffffff",
"glyph_bg": "#000000",
"glyph_edge": "#ffffff",
"glyph_text": "#ffffff",
"grid": "#000000",
"grid_major": "#ffffff",
"hover": "#000000",
"hover_edge": "#ffffff",
"hover_edge_halo": "#000000",
"hover_profile": "#ffffff",
"hover_profile_edge": "#000000",
"icon": "#ffffff",
"icons.agent": "#ffffff",
"icons.highlight": "#000000",
"icons.marker_halo": "#000000",
"interference": "#ffffff",
"logo": [
"#ffffff",
"#ffffff",
"#ffffff"
],
"manipulator": "#ffffff",
"mesh_accent": "#ffffff",
"mesh_fill": "#ffffff",
"mesh_ink": "#ffffff",
"mesh_soft": "#000000",
"on_accent": "#000000",
"origin_plane": "#000000",
"origin_plane_edge": "#ffffff",
"origin_plane_hover": "#ffffff",
"origin_point": "#ffffff",
"overlay": "#000000",
"panel": "#000000",
"panel_header": "#000000",
"preview_add": "#ffffff",
"preview_cut": "#ffffff",
"preview_new": "#ffffff",
"preview_sketch": "#ffffff",
"profile_fill": "#000000",
"render_accent": "#ffffff",
"render_fill": "#ffffff",
"render_ink": "#ffffff",
"render_soft": "#000000",
"rubber_band": "#ffffff",
"section_cap": "#ffffff",
"sel_edge": "#000000",
"sel_edge_rim": "#000000",
"sel_face": "#ffffff",
"sel_profile": "#ffffff",
"sel_profile_edge": "#000000",
"sel_vertex": "#ffffff",
"selected_bg": "#ffffff",
"selected_ink": "#000000",
"sheet_fill": "#ffffff",
"sheet_ink": "#ffffff",
"sheet_soft": "#000000",
"sketch": "#ffffff",
"sketch_accent": "#ffffff",
"sketch_centerline": "#ffffff",
"sketch_construction": "#ffffff",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#ffffff",
"sketch_locked": "#ffffff",
"sketch_lost": "#ffffff",
"sketch_point": "#ffffff",
"sketch_point_free": "#ffffff",
"sketch_projected": "#ffffff",
"sketch_soft": "#000000",
"surface_accent": "#ffffff",
"surface_fill": "#ffffff",
"surface_ink": "#ffffff",
"surface_soft": "#000000",
"tab_on_fill_dark": "#ffffff",
"text": "#ffffff",
"text_dim": "#ffffff",
"timeline": "#000000",
"timeline_item": "#000000",
"timeline_item_error": "#000000",
"timeline_marker": "#ffffff",
"toolbar": "#000000",
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffffff"
},
"format": "solidcraft-theme/1",
"name": "1-bit Dark",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#ffffff",
"line": "#ffffff",
"passive-1": "#000000",
"passive-2": "#000000",
"passive-3": "#000000",
"guide": "#ffffff",
"solid": [
"#000000",
"#000000",
"#ffffff"
],
"surface": [
"#000000",
"#000000",
"#ffffff"
],
"mesh": [
"#000000",
"#000000",
"#ffffff"
],
"form": [
"#000000",
"#000000",
"#ffffff"
],
"sketch": [
"#000000",
"#000000",
"#ffffff"
],
"constraint": [
"#000000",
"#000000",
"#ffffff"
],
"construction": [
"#000000",
"#000000",
"#ffffff"
],
"active-1": "#000000",
"active-2": "#000000",
"active-3": "#ffffff",
"component-orange-1": "#000000",
"component-orange-2": "#000000",
"component-orange-3": "#ffffff",
"component-yellow-1": "#000000",
"component-yellow-2": "#000000",
"component-yellow-3": "#ffffff"
},
"borders": {
"primary": {
"width": 1,
"color": "#ffffff"
},
"secondary": {
"width": 1,
"color": "#ffffff"
},
"tertiary": {
"width": 1,
"color": "#ffffff"
},
"container": {
"width": 1,
"color": "#ffffff"
},
"control": {
"width": 1,
"color": "#ffffff"
}
}
}
+247
View File
@@ -0,0 +1,247 @@
{
"base": "light",
"colors": {
"accent": "#000000",
"accent_soft": "#ffffff",
"agent_busy": "#000000",
"agent_ok": "#000000",
"app_bar": "#ffffff",
"app_bar_hover": "#ffffff",
"app_bar_hover_button": "#ffffff",
"app_bar_hover_strong": "#ffffff",
"app_bar_hover_subtle": "#ffffff",
"app_bar_separator": "#000000",
"app_bar_tab_active": "#ffffff",
"app_bar_text": "#000000",
"axis_x": "#000000",
"axis_y": "#000000",
"axis_z": "#000000",
"bend_down": "#000000",
"bend_up": "#000000",
"body": "#ffffff",
"body_edge": "#000000",
"body_selected": "#000000",
"border": "#000000",
"box_crossing": "#000000",
"box_window": "#000000",
"chip": "#000000",
"construction_geom": "#000000",
"construction_plane": "#ffffff",
"continuity": [
"#000000",
"#000000",
"#000000",
"#000000"
],
"control_border": "#000000",
"control_fill": "#ffffff",
"cube_border": "#000000",
"cube_face": "#ffffff",
"cube_hover": "#ffffff",
"cube_text": "#000000",
"dialog_bg": "#ffffff",
"dialog_body": "#ffffff",
"dialog_border": "#000000",
"dialog_footer": "#ffffff",
"dialog_label": "#000000",
"dialog_rule": "#000000",
"dimension": "#000000",
"dimension_driven": "#000000",
"error": "#000000",
"field": "#ffffff",
"file_background": "#ffffff",
"file_border": "#000000",
"file_dim": "#000000",
"file_image_tint": "#ffffff",
"file_primary": "#ffffff",
"file_primary_border": "#000000",
"file_primary_ink": "#000000",
"file_rail": "#ffffff",
"file_rail_hover": "#ffffff",
"file_rail_ink": "#000000",
"file_recovery": "#ffffff",
"file_recovery_border": "#000000",
"file_recovery_ink": "#000000",
"file_recovery_well": "#ffffff",
"file_surface": "#ffffff",
"form_accent": "#000000",
"form_dim": "#000000",
"form_error": "#000000",
"form_error_fill": "#ffffff",
"form_fill": "#000000",
"form_ink": "#000000",
"form_primary": "#ffffff",
"form_primary_fill": "#000000",
"form_soft": "#ffffff",
"form_success": "#000000",
"form_success_fill": "#ffffff",
"form_tab_ink": "#000000",
"glyph_bg": "#ffffff",
"glyph_edge": "#000000",
"glyph_text": "#000000",
"grid": "#ffffff",
"grid_major": "#000000",
"hover": "#ffffff",
"hover_edge": "#000000",
"hover_edge_halo": "#ffffff",
"hover_profile": "#000000",
"hover_profile_edge": "#ffffff",
"icon": "#000000",
"icons.agent": "#000000",
"icons.highlight": "#ffffff",
"icons.marker_halo": "#ffffff",
"interference": "#000000",
"logo": [
"#000000",
"#000000",
"#000000"
],
"manipulator": "#000000",
"mesh_accent": "#000000",
"mesh_fill": "#000000",
"mesh_ink": "#000000",
"mesh_soft": "#ffffff",
"on_accent": "#ffffff",
"origin_plane": "#ffffff",
"origin_plane_edge": "#000000",
"origin_plane_hover": "#000000",
"origin_point": "#000000",
"overlay": "#ffffff",
"panel": "#ffffff",
"panel_header": "#ffffff",
"preview_add": "#000000",
"preview_cut": "#000000",
"preview_new": "#000000",
"preview_sketch": "#000000",
"profile_fill": "#ffffff",
"render_accent": "#000000",
"render_fill": "#000000",
"render_ink": "#000000",
"render_soft": "#ffffff",
"rubber_band": "#000000",
"section_cap": "#000000",
"sel_edge": "#ffffff",
"sel_edge_rim": "#ffffff",
"sel_face": "#000000",
"sel_profile": "#000000",
"sel_profile_edge": "#ffffff",
"sel_vertex": "#000000",
"selected_bg": "#000000",
"selected_ink": "#ffffff",
"sheet_fill": "#000000",
"sheet_ink": "#000000",
"sheet_soft": "#ffffff",
"sketch": "#000000",
"sketch_accent": "#000000",
"sketch_centerline": "#000000",
"sketch_construction": "#000000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
"sketch_ink": "#000000",
"sketch_locked": "#000000",
"sketch_lost": "#000000",
"sketch_point": "#000000",
"sketch_point_free": "#000000",
"sketch_projected": "#000000",
"sketch_soft": "#ffffff",
"surface_accent": "#000000",
"surface_fill": "#000000",
"surface_ink": "#000000",
"surface_soft": "#ffffff",
"tab_on_fill_dark": "#000000",
"text": "#000000",
"text_dim": "#000000",
"timeline": "#ffffff",
"timeline_item": "#ffffff",
"timeline_item_error": "#ffffff",
"timeline_marker": "#000000",
"toolbar": "#ffffff",
"viewport_bottom": "#ffffff",
"viewport_top": "#ffffff",
"warning": "#000000"
},
"format": "solidcraft-theme/1",
"name": "1-bit",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#000000",
"line": "#000000",
"passive-1": "#ffffff",
"passive-2": "#ffffff",
"passive-3": "#ffffff",
"guide": "#000000",
"solid": [
"#ffffff",
"#ffffff",
"#000000"
],
"surface": [
"#ffffff",
"#ffffff",
"#000000"
],
"mesh": [
"#ffffff",
"#ffffff",
"#000000"
],
"form": [
"#ffffff",
"#ffffff",
"#000000"
],
"sketch": [
"#ffffff",
"#ffffff",
"#000000"
],
"constraint": [
"#ffffff",
"#ffffff",
"#000000"
],
"construction": [
"#ffffff",
"#ffffff",
"#000000"
],
"active-1": "#ffffff",
"active-2": "#ffffff",
"active-3": "#000000",
"component-orange-1": "#ffffff",
"component-orange-2": "#ffffff",
"component-orange-3": "#000000",
"component-yellow-1": "#ffffff",
"component-yellow-2": "#ffffff",
"component-yellow-3": "#000000"
},
"borders": {
"primary": {
"width": 1,
"color": "#000000"
},
"secondary": {
"width": 1,
"color": "#000000"
},
"tertiary": {
"width": 1,
"color": "#000000"
},
"container": {
"width": 1,
"color": "#000000"
},
"control": {
"width": 1,
"color": "#000000"
}
}
}
+3
View File
@@ -54,6 +54,9 @@
"dimension_driven": "#6c6863",
"error": "#c71529",
"field": "#fffdfa",
"file_rail": "#e1ddd8",
"file_rail_hover": "#ebe7e2",
"file_rail_ink": "#13110e",
"form_accent": "#5844bc",
"form_dim": "#6c6863",
"form_error": "#c71529",
+216
View File
@@ -0,0 +1,216 @@
{
"base": "dark",
"colors": {
"accent": "#55ffff",
"accent_soft": "#ff55ff",
"agent_busy": "#55ffff",
"agent_ok": "#55ff55",
"app_bar": "#000000",
"app_bar_hover": "#ffffff18",
"app_bar_hover_button": "#ffffff22",
"app_bar_hover_strong": "#ffffff30",
"app_bar_hover_subtle": "#ffffff10",
"app_bar_separator": "#00aaaa",
"app_bar_tab_active": "#aa00aa",
"app_bar_text": "#ffffff",
"axis_x": "#ff5555",
"axis_y": "#55ff55",
"axis_z": "#5555ff",
"bend_down": "#ffff55",
"bend_up": "#55ff55",
"body": "#aa00aa",
"body_edge": "#55ffff",
"body_selected": "#ffff55",
"border": "#55ffff",
"box_crossing": "#ff55ff",
"box_window": "#55ffff",
"chip": "#aa00aa",
"construction_geom": "#aaaaaa",
"construction_plane": "#ffff5522",
"continuity": [
"#55ff55",
"#5555ff",
"#ffff55",
"#ff55ff"
],
"control_border": "#55ffff",
"control_fill": "#000000",
"cube_border": "#55ffff",
"cube_face": "#000000",
"cube_hover": "#aa00aa",
"cube_text": "#ffffff",
"dialog_bg": "#000000",
"dialog_body": "#000000",
"dialog_border": "#55ffff",
"dialog_footer": "#000000",
"dialog_label": "#55ffff",
"dialog_rule": "#00aaaa",
"dimension": "#ffffff",
"dimension_driven": "#aaaaaa",
"error": "#ff5555",
"field": "#000000",
"file_background": "#000000",
"file_border": "#55ffff",
"file_dim": "#55ffff",
"file_image_tint": "#ffffff",
"file_primary": "#000000",
"file_primary_border": "#55ffff",
"file_primary_ink": "#55ffff",
"file_rail": "#000000",
"file_rail_hover": "#aa00aa",
"file_rail_ink": "#ffffff",
"file_recovery": "#000000",
"file_recovery_border": "#ff55ff",
"file_recovery_ink": "#ff55ff",
"file_recovery_well": "#000000",
"file_surface": "#000000",
"form_accent": "#aa0000",
"form_dim": "#55ffff",
"form_error": "#ff5555",
"form_error_fill": "#000000",
"form_fill": "#ff5555",
"form_ink": "#ffffff",
"form_primary": "#ff55ff",
"form_primary_fill": "#ff55ff",
"form_soft": "#000000",
"form_success": "#55ff55",
"form_success_fill": "#000000",
"form_tab_ink": "#ff5555",
"glyph_bg": "#000000",
"glyph_edge": "#ff55ff",
"glyph_text": "#ff55ff",
"grid": "#555555",
"grid_major": "#00aaaa",
"hover": "#aa00aa",
"hover_edge": "#ffffff",
"hover_edge_halo": "#000000",
"hover_profile": "#ffffff40",
"hover_profile_edge": "#ffffff",
"icon": "#ffffff",
"icons.agent": "#ff55ff",
"icons.highlight": "#ffffff",
"icons.marker_halo": "#000000cc",
"interference": "#ff5555",
"logo": [
"#ffffff",
"#55ffff",
"#ff55ff"
],
"manipulator": "#55ffff",
"mesh_accent": "#00aa00",
"mesh_fill": "#55ff55",
"mesh_ink": "#55ff55",
"mesh_soft": "#000000",
"on_accent": "#000000",
"origin_plane": "#55ffff20",
"origin_plane_edge": "#55ffff80",
"origin_plane_hover": "#ff55ff40",
"origin_point": "#ffffff",
"overlay": "#000000",
"panel": "#000000",
"panel_header": "#000000",
"preview_add": "#55ffff",
"preview_cut": "#ff555566",
"preview_new": "#ffff55",
"preview_sketch": "#55ff55",
"profile_fill": "#55ffff22",
"render_accent": "#aaaaaa",
"render_fill": "#ffffff",
"render_ink": "#ffffff",
"render_soft": "#000000",
"rubber_band": "#55ffff",
"section_cap": "#ffff55",
"sel_edge": "#ffff55",
"sel_edge_rim": "#000000",
"sel_face": "#ffff55",
"sel_profile": "#ffff55",
"sel_profile_edge": "#ffff55",
"sel_vertex": "#ffff55",
"selected_bg": "#ff55ff",
"selected_ink": "#000000",
"sheet_fill": "#aaaaaa",
"sheet_ink": "#aaaaaa",
"sheet_soft": "#000000",
"sketch": "#55ffff",
"sketch_accent": "#ff55ff",
"sketch_centerline": "#ffff55",
"sketch_construction": "#aaaaaa",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#ff55ff",
"sketch_locked": "#55ff55",
"sketch_lost": "#ff5555",
"sketch_point": "#ffffff",
"sketch_point_free": "#55ffff",
"sketch_projected": "#ff55ff",
"sketch_soft": "#000000",
"surface_accent": "#aa5500",
"surface_fill": "#ffff55",
"surface_ink": "#ffff55",
"surface_soft": "#000000",
"tab_on_fill_dark": "#000000",
"text": "#ffffff",
"text_dim": "#55ffff",
"timeline": "#000000",
"timeline_item": "#000000",
"timeline_item_error": "#aa0000",
"timeline_marker": "#55ffff",
"toolbar": "#000000",
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffff55"
},
"format": "solidcraft-theme/1",
"name": "CGA",
"radii": {
"radius_sm": 0,
"radius_md": 0,
"radius_lg": 0,
"radius_xl": 0,
"control_radius": 0,
"sketch_mode_radius": 0
},
"icon_palette": {
"ink": "#000000",
"line": "#ffffff",
"passive-1": "#ffffff",
"passive-2": "#aaaaaa",
"passive-3": "#555555",
"guide": "#55ffff",
"solid": [
"#ffffff",
"#55ffff",
"#00aaaa"
],
"surface": [
"#ffffff",
"#ffff55",
"#aa5500"
],
"mesh": [
"#ffffff",
"#55ff55",
"#00aa00"
],
"form": [
"#ffffff",
"#ff5555",
"#aa0000"
],
"sketch": [
"#ffffff",
"#ff55ff",
"#aa00aa"
],
"constraint": [
"#ffffff",
"#ff55ff",
"#aa00aa"
],
"construction": [
"#ffffff",
"#ffff55",
"#aa5500"
]
}
}
+3
View File
@@ -54,6 +54,9 @@
"dimension_driven": "#8f9296",
"error": "#f85052",
"field": "#2c2e31",
"file_rail": "#0f1113",
"file_rail_hover": "#222527",
"file_rail_ink": "#e1e3e5",
"form_accent": "#a6a2fc",
"form_dim": "#8f9296",
"form_error": "#ff6060",
+74 -13
View File
@@ -201,20 +201,24 @@ impl Document {
/// The faces of a body with appearances of their own: (B-rep face index, look). A later
/// assignment to the same face wins.
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
// Face colours the body was imported with, under the design's own.
// An assigned body appearance replaces imported paint; design face appearances still win.
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
let mut out: Vec<(usize, Look)> = match b.body.paint() {
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
// Carried from an earlier version of the body: by face name (worked out only here:
// the viewport asks every body for its face colours on every change).
None => match &b.imported {
Some(imp) => crate::naming::face_names(b)
.iter()
.enumerate()
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
.collect(),
None => Vec::new(),
},
let mut out: Vec<(usize, Look)> = if self.appearances.bodies.contains_key(b.definition_name()) {
Vec::new()
} else {
match b.body.paint() {
Some(p) => p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect(),
// Carried from an earlier version of the body: by face name (worked out only here:
// the viewport asks every body for its face colours on every change).
None => match &b.imported {
Some(imp) => crate::naming::face_names(b)
.iter()
.enumerate()
.filter_map(|(i, n)| imp.faces.get(crate::naming::strip_piece(n)).map(|(c, o)| (i, Look::custom(c.map(to8), *o as f64))))
.collect(),
None => Vec::new(),
},
}
};
for f in self.appearances.faces.iter().filter(|f| f.body == b.definition_name()) {
let (p, look) = (crate::apply_point(&b.source_transform, f.point), f.look.clone());
@@ -339,6 +343,63 @@ mod tests {
assert!(!mixed.differs_only_in_appearance(&before), "a material change is not appearance-only");
}
fn imported_box() -> crate::ModelBody {
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
let faces = body
.faces(0.01)
.unwrap()
.iter()
.map(|face| solidcraft_kernel::FacePaint { face: face.index, color: [1., 0., 0.], opacity: 0.5 })
.collect();
crate::ModelBody::new("Imported".into(), body.with_paint(Some(solidcraft_kernel::Paint { opacity: 1., faces })), 1)
}
fn check_imported_appearance_precedence(body: &crate::ModelBody) {
let mut doc = Document::default();
let face_count = body.body.faces(0.01).unwrap().len();
let imported = doc.face_colors(body);
assert_eq!(imported.len(), face_count);
assert!(imported.iter().all(|(_, look)| *look == Look::custom([255, 0, 0], 0.5)));
let body_look = Look::custom([0, 0, 255], 0.8);
doc.appearances.bodies.insert(body.definition_name().into(), body_look.clone());
assert_eq!(doc.model_body_look(body), Some(body_look.clone()));
assert_eq!(doc.model_body_opacity(body), body_look.opacity);
assert!(doc.face_colors(body).is_empty(), "all faces must inherit the assigned body appearance");
let face_look = Look::custom([0, 255, 0], 1.);
let name = crate::naming::face_names(body)[0].clone();
doc.appearances.faces.push(FaceLook {
body: body.definition_name().into(),
point: Vec3::ZERO,
look: face_look.clone(),
name: Some(name),
strict_name: false,
certificate: None,
});
assert_eq!(doc.face_colors(body), vec![(0, face_look.clone())]);
doc.appearances.bodies.clear();
let restored = doc.face_colors(body);
assert_eq!(restored.len(), face_count);
assert!(restored.iter().all(|(face, look)| *look == if *face == 0 { face_look.clone() } else { Look::custom([255, 0, 0], 0.5) }));
}
#[test]
fn body_appearance_overrides_imported_face_paint() {
check_imported_appearance_precedence(&imported_box());
}
#[test]
fn body_appearance_overrides_carried_imported_face_paint() {
let mut body = imported_box();
body.imported = ImportedLook::of(&body);
body.body = body.body.clone().with_paint(None);
body.source_name = Some(body.name.clone());
body.name = "Occurrence".into();
check_imported_appearance_precedence(&body);
}
#[test]
fn library_and_materials() {
assert_eq!(library("abs - red").map(|l| l.color), Some([196, 40, 40]));
+17 -9
View File
@@ -58,6 +58,22 @@ impl Document {
let occurrences: BTreeSet<u64> =
self.occurrences.iter().filter(|o| inside.contains(&o.component) || inside.contains(&o.parent)).map(|o| o.id).collect();
let joints = self.remove_occurrences(&occurrences);
let bodies: BTreeSet<String> = self.body_components.iter().filter(|(_, c)| inside.contains(*c)).map(|(name, _)| name.clone()).collect();
self.body_components.retain(|_, c| !inside.contains(c));
self.body_offsets.retain(|name, _| !bodies.contains(name));
self.components.retain(|c| !inside.contains(&c.id));
self.parts.retain(|c, _| !inside.contains(c));
self.links.retain(|c, _| !inside.contains(c));
self.remove_configuration_components(&components);
Ok(RemovedTree { components, features, deleted, occurrences: occurrences.into_iter().collect(), joints: joints.into_iter().collect() })
}
/// Remove these occurrences and every assembly record that refers to them: the joints with
/// either side on one (with those joints' motion links and motion keys), the joint origins
/// on them, their places in contact sets and their exploded moves. Returns the joints removed.
pub fn remove_occurrences(&mut self, occurrences: &BTreeSet<u64>) -> BTreeSet<u64> {
let joints: BTreeSet<u64> = self
.assembly
.joints
@@ -79,14 +95,6 @@ impl Document {
view.moves.retain(|(o, _)| !occurrences.contains(o));
}
self.occurrences.retain(|o| !occurrences.contains(&o.id));
let bodies: BTreeSet<String> = self.body_components.iter().filter(|(_, c)| inside.contains(*c)).map(|(name, _)| name.clone()).collect();
self.body_components.retain(|_, c| !inside.contains(c));
self.body_offsets.retain(|name, _| !bodies.contains(name));
self.components.retain(|c| !inside.contains(&c.id));
self.parts.retain(|c, _| !inside.contains(c));
self.links.retain(|c, _| !inside.contains(c));
self.remove_configuration_components(&components);
Ok(RemovedTree { components, features, deleted, occurrences: occurrences.into_iter().collect(), joints: joints.into_iter().collect() })
joints
}
}
+12 -4
View File
@@ -693,6 +693,11 @@ pub enum FeatureKind {
height: Expr,
#[serde(default)]
operation: Operation,
/// Up to two straight edges of the face it stands on (#6380), as a Single hole's
/// references: the base centre sits at each one's distance from its edge, in the face's
/// plane, and follows the edges when the part changes. `base` keeps where it was placed.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
references: Vec<HoleReference>,
},
Sphere {
#[serde(with = "point_expr")]
@@ -3505,11 +3510,14 @@ impl Document {
let id = f.id;
let insertion_group = self.marker.and_then(|marker| self.timeline_group_at_insertion(marker));
match self.marker {
Some(m) if m < self.features.len() => {
self.features.insert(m, f);
self.marker = Some(m + 1);
// A marker right after the last feature moves on past a feature added there, or the
// new feature would be added rolled back (#7741).
Some(marker) => {
let position = marker.min(self.features.len());
self.features.insert(position, f);
self.marker = Some(position + 1);
}
_ => self.features.push(f),
None => self.features.push(f),
}
if let Some(group_id) = insertion_group {
let positions: std::collections::BTreeMap<u64, usize> =
+83 -25
View File
@@ -2,6 +2,8 @@
#[path = "placed_mesh.rs"]
mod placed_mesh;
#[path = "placement_reuse.rs"]
mod placement_reuse;
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
@@ -1047,6 +1049,21 @@ const CACHE_ENTRIES: usize = 4096;
const CACHE_BYTES: usize = 64 << 20;
impl EvalCache {
/// A cache that keeps `bytes` of results (always at least the last one): a small budget
/// stands in for a design whose states are large.
pub fn with_budget(bytes: usize) -> Self {
EvalCache { budget: bytes, ..EvalCache::default() }
}
/// Results stored.
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
fn get(&mut self, key: u64) -> Option<CachedEval> {
let r = self.map.get(&key).cloned();
if r.is_some() {
@@ -1288,6 +1305,10 @@ pub struct Model {
primed: Option<(u64, Arc<std::collections::HashMap<u64, FeatureResult>>)>,
/// Shared with copies of this evaluator; states hold only a weak reference.
frame_bodies: Arc<std::sync::Mutex<FrameBodies>>,
/// Inputs proven to describe placement-independent geometry, and the state they produced.
placement_inputs: Option<placement_reuse::Inputs>,
/// All evaluated features use supported local inputs; computed with the existing owner index.
placement_local: bool,
}
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
@@ -2008,6 +2029,7 @@ impl Model {
pub fn adopt_solved_sketches(&mut self, doc: &mut Document) -> usize {
let solved: Vec<SolvedSketch> = self.edited_sketches(doc).cloned().collect();
let Some(first) = solved.iter().filter_map(|s| doc.features.iter().position(|f| f.id == s.feature)).min() else { return 0 };
self.placement_inputs = None;
for s in &solved {
if let Ok(stored) = doc.sketch_mut(s.feature) {
adopt_solution(stored, &s.sketch);
@@ -2095,6 +2117,8 @@ impl Model {
/// Re-evaluate the document, reusing results up to the first changed feature.
pub fn evaluate(&mut self, doc: &Document) {
self.placement_inputs = None;
self.placement_local = true;
let (vals, perr) = evaluation_values(doc);
self.param_errors = perr;
self.frame_bodies.lock().unwrap_or_else(|e| e.into_inner()).next_generation();
@@ -2122,6 +2146,7 @@ impl Model {
}
let watched = crate::progress::begin(doc.features.len());
for (i, f) in doc.features.iter().enumerate() {
self.placement_local = self.placement_local && placement_reuse::local_geometry(f, &feature_component);
let rolled_back = i >= marker;
// Retain at most one active result per rolled-back feature, even across several
// marker moves. Build the map anew so deleted features release their states too.
@@ -2338,6 +2363,7 @@ impl Model {
state: state.clone(),
});
}
self.placement_local = self.placement_local && out.iter().all(|r| !r.skipped && !r.stale && !r.unkept && r.error.is_none());
self.results = out;
self.last_recomputed = recomputed;
self.recomputed += recomputed;
@@ -2350,35 +2376,44 @@ impl Model {
/// design after the rename, which changed nothing but names, so every result keeps its
/// geometry and meshes and only the body's name (and the names worked out from it) changes.
/// Later features that change the body (a pattern, a chamfer, a join) keep their results
/// too (#2467): a feature's geometry doesn't depend on its bodies' names.
/// too (#2467): a feature's geometry doesn't depend on its bodies' names. A body no later
/// feature changes is followed even when other bodies are numbered after it (`X (2)`, the
/// solids of a STEP file that share a label): `body.rename` keeps their names.
/// False, leaving the model as it was, when the rename can't be followed that way and the
/// design must be evaluated: the model isn't the design's, a body of that name comes and goes
/// or isn't the same body throughout, a later body's name is worked out from it (`X (2)`), a
/// message quotes it, or it is sheet metal, threaded, or named in a later feature's face
/// references.
/// or isn't the same body throughout, a later body's name is worked out from a body later
/// features change, a message quotes it, or it is sheet metal, threaded, or named in a later
/// feature's face references.
pub fn rename_body(&mut self, doc: &Document, old: &str, new: &str) -> bool {
if self.results.len() != doc.features.len() || self.results.iter().zip(&doc.features).any(|(r, f)| r.id != f.id || r.stale) {
return false;
}
let Some(at) = self.results.iter().position(|r| r.state.body(old).is_some()) else { return false };
let Some(feature) = self.results.get(at).and_then(|r| r.state.body(old)).map(|b| b.feature) else { return false };
// One body from the feature that made it until it is gone (joined into another), and no
// other body's name comes from its name.
let Some((feature, made)) = self.results.get(at).and_then(|r| r.state.body(old)).map(|b| (b.feature, Arc::as_ptr(&b.mesh))) else {
return false;
};
// Untouched from the feature that made it to the end: `body.rename` keeps the names of
// its siblings and of later bodies named after it (`X (2)`, the solids of a STEP file
// with one label), so evaluating the renamed design gives the same names.
let untouched = self.results.iter().skip(at).all(|r| r.state.body(old).is_some_and(|b| Arc::as_ptr(&b.mesh) == made));
// Or one body from the feature that made it until it is gone (joined into another), and
// no other body's name comes from its name.
let held: Vec<bool> = self.results.iter().skip(at).map(|r| r.state.body(old).is_some()).collect();
let one_run = held.iter().skip_while(|h| **h).all(|h| !*h);
let derived = format!("{old} (");
let same_body = self
.results
.iter()
.skip(at)
.all(|r| r.state.bodies.iter().all(|b| if b.name == old { b.feature == feature } else { !b.name.starts_with(&derived) }));
let same_body = || {
self.results
.iter()
.skip(at)
.all(|r| r.state.bodies.iter().all(|b| if b.name == old { b.feature == feature } else { !b.name.starts_with(&derived) }))
};
let quoted = self.results.iter().skip(at).any(|r| [&r.error, &r.warning].into_iter().flatten().any(|m| m.contains(old)));
let named_elsewhere = self
.results
.iter()
.any(|r| r.state.body(new).is_some() || r.state.sheets.iter().any(|x| x.body == old) || r.state.threads.iter().any(|t| t.body == old));
let faces_named = doc.features.iter().skip(at + 1).any(|f| f.face_names.iter().any(|n| n.contains(old)));
if !one_run || !same_body || quoted || named_elsewhere || faces_named {
if !(untouched || (one_run && same_body())) || quoted || named_elsewhere || faces_named {
return false;
}
// States shared between results stay shared.
@@ -2450,6 +2485,7 @@ impl Model {
/// Set every result's fingerprint to `doc`'s, so the next evaluation reuses every result
/// (after a change that builds nothing differently).
pub fn refingerprint(&mut self, doc: &Document) {
self.placement_inputs = None;
let (vals, _) = evaluation_values(doc);
let marker = doc.marker.unwrap_or(usize::MAX);
self.step_digests = StepDigests::of(doc, &self.step_digests);
@@ -4430,17 +4466,24 @@ fn single_hole_position(
Ok(ss.plane.to_world(p.pos))
}
/// A hole's reference edges (#37); none for any other feature.
/// A hole's (#37) or a cylinder's (#6380) reference edges; none for any other feature.
fn hole_references(f: &Feature) -> &[crate::HoleReference] {
match &f.kind {
FeatureKind::Hole { references, .. } => references,
FeatureKind::Hole { references, .. } | FeatureKind::Cylinder { references, .. } => references,
_ => &[],
}
}
/// Where a Single hole with reference edges (#37) is drilled: at its distances from the edges'
/// lines, in the plane of the face at `at` (on the surface), the edges found by their names, else
/// their points. A hole without reference edges stays at `at`.
/// Where a cylinder stands: its base, or with reference edges (#6380) at their distances in the
/// plane of the flat face at its base, square to its axis.
fn cylinder_base(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, base: &crate::PointExpr, axis: Vec3) -> Result<Vec3> {
let at = point(vals, base)?;
with_references(vals, st, f, at, -axis)
}
/// Where a Single hole (or a cylinder) with reference edges (#37) is placed: at its distances
/// from the edges' lines, in the plane of the face at `at` (on the surface), the edges found by
/// their names, else their points. A feature without reference edges stays at `at`.
fn with_references(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, at: Vec3, dir: Vec3) -> Result<Vec3> {
let references = hole_references(f);
if references.is_empty() {
@@ -5747,7 +5790,7 @@ fn feature_tools_with_constructor(
Ok(vec![crate::lineage::box_tool(kernel::box_solid(corner, corner + s)?)])
}
FeatureKind::Cylinder { base, axis, radius, height, .. } => Ok(vec![crate::lineage::cylinder_tool(kernel::cylinder(
point(vals, base)?,
cylinder_base(vals, st, f, base, axis.0)?,
axis.0,
val(vals, radius, Kind::Length)?,
val(vals, height, Kind::Length)?,
@@ -7649,8 +7692,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
apply_traced(doc, st, f, tools, *operation, &[])
}
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
let (base, radius, height) = (point(vals, base)?, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?);
FeatureKind::Cylinder { base, axis, radius, height, operation, .. } => {
let base = cylinder_base(vals, st, f, base, axis.0)?;
let (radius, height) = (val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?);
let b = kernel::cylinder(base, axis.0, radius, height)?;
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
let prism = more::PrimitivePrism::Cylinder { base, axis: axis.0, radius, height };
@@ -8271,26 +8315,39 @@ impl PlacedBodies {
/// Bodies [`state_in_frame_at`] moved into a feature's frame, by source body and matrix: a
/// sketch on a face of a turned component is evaluated in that component's frame, and without
/// this every edit of it moved (and so meshed again) every body placed differently from it.
/// Held by the [`Model`] across evaluations; each `evaluate` starts a new generation, and an
/// entry neither it nor the evaluation before it used is dropped, so the cache holds only the
/// placements in use.
/// Held by the [`Model`] across evaluations; each `evaluate` that moves a body starts a new
/// generation, and an entry neither it nor the last such evaluation before it used is dropped,
/// so the cache holds only the placements in use. An evaluation that moves nothing (every
/// result reused, or taken from the cache: an undo, the refreshes a command makes after its
/// edit) leaves the generations alone: counting it, two of them in a row dropped every moved
/// body, and the next edit of the sketch moved and meshed them all again.
#[derive(Debug, Default)]
pub(crate) struct FrameBodies {
now: PlacedBodies,
before: PlacedBodies,
/// An evaluation began and has not used the cache yet: its first use starts the generation.
pending: bool,
}
impl FrameBodies {
/// At most this many bodies are kept per generation.
const CAP: usize = 4096;
/// An evaluation begins; its generation starts when it first moves a body.
pub(crate) fn next_generation(&mut self) {
self.before = std::mem::take(&mut self.now);
self.pending = true;
}
fn begin(&mut self) {
if std::mem::take(&mut self.pending) {
self.before = std::mem::take(&mut self.now);
}
}
/// The moved body for `b` under `m`, when a call in this or the previous generation moved
/// the same body by the same matrix.
fn reused(&mut self, b: &ModelBody, m: &crate::Mat) -> Option<ModelBody> {
self.begin();
if let Some(placed) = self.now.reused(b, m) {
return Some(placed.clone());
}
@@ -8305,6 +8362,7 @@ impl FrameBodies {
}
fn keep(&mut self, b: &ModelBody, m: &crate::Mat, moved: &ModelBody) {
self.begin();
if self.now.by_source.len() < Self::CAP {
self.now.by_source.insert(placed_key(b, m), (b.clone(), moved.clone()));
}
+175 -2
View File
@@ -59,6 +59,7 @@ fn joint_face_measurements_are_shared_across_motion_samples_and_snap_kinds() {
radius: "2".into(),
height: "10".into(),
operation: Operation::NewBody,
references: Vec::new(),
},
None,
)
@@ -225,8 +226,10 @@ fn a_sketch_on_a_face_still_sees_the_placed_bodies() {
assert_eq!(work.digested, 0);
}
#[test]
fn editing_a_sketch_on_a_turned_occurrence_reuses_its_frame_bodies() {
/// A sketch on a face of a plate placed turned (its own frame, so the plate is moved into it),
/// evaluated once: the design, its model, the plate's component, the sketch, its anchor and the
/// face picked.
fn sketch_on_a_turned_occurrence() -> (Document, Model, u64, u64, FaceAnchor, Plane) {
use solidcraft_sketch::{LinkKind, LinkSource};
let mut doc = Document::new("turned face");
@@ -262,6 +265,12 @@ fn editing_a_sketch_on_a_turned_occurrence_reuses_its_frame_bodies() {
let first = counted(|| model.evaluate(&doc));
assert!(first.moved > 0, "{first:?}");
assert!(model.result(id).unwrap().error.is_none(), "{:?}", model.result(id).unwrap().error);
(doc, model, comp, id, anchor, picked)
}
#[test]
fn editing_a_sketch_on_a_turned_occurrence_reuses_its_frame_bodies() {
let (mut doc, model, comp, id, anchor, picked) = sketch_on_a_turned_occurrence();
let reopened = Model::adopt(&doc, &model.saved(&doc).unwrap()).unwrap();
for mut model in [model, reopened] {
let before = state_in_face_anchor_frame(&doc, &model.state_before(id), &anchor, 0).unwrap().bodies.first().unwrap().mesh();
@@ -291,6 +300,27 @@ fn editing_a_sketch_on_a_turned_occurrence_reuses_its_frame_bodies() {
}
}
/// Evaluations between two edits of the sketch that move nothing (a delete refreshes the model
/// more than once, an undo can take its sketch from the cache) leave its frame bodies in place:
/// they once aged them out, and the next edit moved and meshed every placed body again, which
/// in a design of thousands of imported bodies took a minute per edit.
#[test]
fn evaluations_that_move_nothing_keep_the_frame_bodies() {
let (mut doc, mut model, _, id, _, _) = sketch_on_a_turned_occurrence();
let edit = |doc: &mut Document, to: Vec2| doc.sketch_mut(id).unwrap().points.get_mut(1).unwrap().pos = to;
edit(&mut doc, Vec2::new(4., 5.));
let work = counted(|| model.evaluate(&doc));
assert_eq!(work.moved, 0, "{work:?}");
for _ in 0..3 {
let work = counted(|| model.evaluate(&doc));
assert_eq!((work.moved, model.last_recomputed), (0, 0), "{work:?}");
}
edit(&mut doc, Vec2::new(6., 7.));
let work = counted(|| model.evaluate(&doc));
assert_eq!(model.last_recomputed, 1);
assert_eq!(work.moved, 0, "the edit after idle evaluations must reuse the moved plate: {work:?}");
}
#[test]
fn an_imported_file_is_fingerprinted_by_its_text() {
let doc = placed_imports(2);
@@ -433,6 +463,7 @@ fn a_body_is_measured_once_while_it_stays_the_same() {
radius: "2".into(),
height: "10".into(),
operation: Operation::NewBody,
references: Vec::new(),
};
doc.add_feature(kind, None).unwrap();
};
@@ -474,3 +505,145 @@ fn placed_bodies_taken_as_before_keep_their_measure() {
let again = world_state_reusing(&doc, &state, &mut cache);
assert_eq!(counted(|| assert_eq!(again.bodies[0].measure().unwrap().centroid, centroid)).measures, 0);
}
#[test]
fn shared_evaluation_requires_its_actual_model_and_unmodified_geometry_inputs() {
let mut doc = Document::new("shared evaluation");
let feature = doc
.add_feature(
FeatureKind::Box {
corner: point_expr(Vec3::ZERO),
length: "2".into(),
width: "3".into(),
height: "4".into(),
operation: Operation::NewBody,
},
None,
)
.unwrap();
let mut doc = Arc::new(doc);
let mut model = Model::new();
assert!(model.evaluate_shared(&doc));
let original = model.state();
assert!(!model.evaluate_shared(&doc));
assert_eq!(model.last_recomputed, 0);
assert!(Arc::ptr_eq(&original, &model.state()));
// A geometry edit must be seen even when a caller did not evaluate it before moving.
let FeatureKind::Box { length, .. } = &mut Arc::make_mut(&mut doc).feature_mut(feature).unwrap().kind else { panic!("box") };
*length = "5".into();
assert!(model.evaluate_shared(&doc));
assert!((model.state().bodies[0].measure().unwrap().volume - 60.).abs() < 1e-8);
assert!((original.bodies[0].measure().unwrap().volume - 24.).abs() < 1e-8);
assert!(!model.evaluate_shared(&doc));
let mut other_doc = (*doc).clone();
let FeatureKind::Box { length, .. } = &mut other_doc.feature_mut(feature).unwrap().kind else { panic!("box") };
*length = "7".into();
let mut other = Model::new();
other.evaluate(&other_doc);
model.results = other.results;
assert!(model.evaluate_shared(&doc), "replaced results cannot use the retained proof");
assert_eq!(model.state().bodies.len(), 1);
model.no_cache = true;
assert!(model.evaluate_shared(&doc));
model.no_cache = false;
assert!(model.evaluate_shared(&doc));
let ahead = model.clone();
model.prime(&ahead);
assert!(model.evaluate_shared(&doc), "primed results use normal evaluation");
model.unprime();
assert!(model.evaluate_shared(&doc));
model.evaluate(&doc);
assert!(model.evaluate_shared(&doc), "the borrowed-document API invalidates the shared-input proof");
Arc::make_mut(&mut doc).set_param("Width", "6 mm", None, None).unwrap();
let FeatureKind::Box { width, .. } = &mut Arc::make_mut(&mut doc).feature_mut(feature).unwrap().kind else { panic!("box") };
*width = "Width".into();
assert!(model.evaluate_shared(&doc));
assert!((model.state().bodies[0].measure().unwrap().volume - 120.).abs() < 1e-8);
Arc::make_mut(&mut doc).set_param("Width", "9 mm", None, None).unwrap();
assert!(model.evaluate_shared(&doc));
assert!((model.state().bodies[0].measure().unwrap().volume - 180.).abs() < 1e-8);
// A primitive's label can change its result without replacing the geometry state.
// Restoring the earlier document must restore that label and fingerprint too.
let name = doc.feature(feature).unwrap().name.clone();
let mut renamed = (*doc).clone();
renamed.feature_mut(feature).unwrap().name = "Renamed box".into();
assert!(model.rename_feature(&renamed, feature));
assert!(model.evaluate_shared(&doc), "refingerprinted results must not keep the old shared-input proof");
assert_eq!(model.result(feature).unwrap().name, name);
assert!(model.is_current(&doc));
}
#[test]
fn shared_imported_mesh_inputs_keep_payload_identity_and_invalidate_replacements() {
let mut doc = Document::new("shared mesh input");
let component = doc.add_component(Some("Part"), 0).unwrap();
let feature = doc
.add_feature(
FeatureKind::MeshImport {
file: "triangle.stl".into(),
meshes: vec![MeshData {
name: "Triangle".into(),
positions: Arc::from([0., 0., 0., 1., 0., 0., 0., 1., 0.]),
triangles: Arc::from([0, 1, 2]),
color: None,
}],
},
None,
)
.unwrap();
doc.feature_mut(feature).unwrap().component = component;
let mut doc = Arc::new(doc);
let mut model = Model::new();
assert!(model.evaluate_shared(&doc));
let original = model.state();
Arc::make_mut(&mut doc).occurrences[0].transform = rigid(Vec3::new(10., 0., 0.), Vec3::ZERO, Vec3::Z, 0.3);
assert!(!model.evaluate_shared(&doc));
assert!(Arc::ptr_eq(&original, &model.state()));
for edit in 0..5 {
let FeatureKind::MeshImport { meshes, .. } = &mut Arc::make_mut(&mut doc).feature_mut(feature).unwrap().kind else { panic!("mesh") };
let mesh = &mut meshes[0];
match edit {
0 => Arc::make_mut(&mut mesh.positions)[3] = 2.,
1 => Arc::make_mut(&mut mesh.triangles).swap(1, 2),
2 => mesh.color = Some([0.2, 0.4, 0.6]),
3 => mesh.name = "Renamed".into(),
_ => mesh.positions = Arc::from(mesh.positions.to_vec()),
}
assert!(model.evaluate_shared(&doc), "mesh edit {edit} must invalidate");
assert!(model.results.iter().all(|r| r.error.is_none()));
let mut fresh = Model::new();
fresh.no_cache = true;
fresh.evaluate(&doc);
let (actual, expected) = (model.state(), fresh.state());
assert_eq!(actual.bodies[0].name, expected.bodies[0].name);
let (actual, expected) = (actual.bodies[0].mesh(), expected.bodies[0].mesh());
assert_eq!(actual.positions, expected.positions);
assert_eq!(actual.triangles, expected.triangles);
assert_eq!(actual.normals, expected.normals);
assert!(!model.evaluate_shared(&doc));
}
}
#[test]
fn shared_evaluation_refreshes_live_sketches_and_keeps_unknown_features_conservative() {
let mut doc = Document::new("live and unknown inputs");
let feature = doc.add_feature(line_sketch(PlaneRef::Origin { name: "XY".into() }, Vec2::new(2., 1.)), None).unwrap();
let mut doc = Arc::new(doc);
let mut model = Model::new();
assert!(model.evaluate_shared(&doc));
assert!(!model.evaluate_shared(&doc));
let solved = model.state().sketch(feature).unwrap().clone();
let mut live = solved.sketch.clone();
live.points[1].pos.y += 2.;
assert!(model.show_live_sketch(feature, live, solved.report.clone()));
assert!(model.is_stale());
assert!(model.evaluate_shared(&doc));
assert!(!model.is_stale());
assert_eq!(model.state().sketch(feature).unwrap().sketch.points, solved.sketch.points);
Arc::make_mut(&mut doc).add_feature(FeatureKind::ConstructionPlane { plane: PlaneRef::Origin { name: "XY".into() } }, None).unwrap();
assert!(model.evaluate_shared(&doc));
assert!(model.results.iter().all(|r| r.error.is_none()));
assert!(model.evaluate_shared(&doc), "an unclassified feature must keep the ordinary evaluation path");
}
@@ -56,6 +56,11 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
),
("box", json!({"type": "box", "corner": ["box.x", "box.y", "box.z"], "length": "box.length", "width": "box.width", "height": "box.height"})),
("cylinder", json!({"type": "cylinder", "base": ["cyl.x", "cyl.y", "cyl.z"], "radius": "cyl.radius", "height": "cyl.height"})),
(
"cylinder_references",
json!({"type": "cylinder", "base": o, "radius": "cyl.radius", "height": "cyl.height",
"references": [{"edge": o, "distance": "cyl.reference1"}, {"edge": o, "distance": "cyl.reference2"}]}),
),
("sphere", json!({"type": "sphere", "center": ["sph.x", "sph.y", "sph.z"], "radius": "sph.radius"})),
("torus", json!({"type": "torus", "center": ["tor.x", "tor.y", "tor.z"], "major": "tor.major", "minor": "tor.minor"})),
(
@@ -286,6 +291,9 @@ pub(super) fn with_points(name: &str, mut v: Value) -> Value {
"surface_revolve" => json!({"axis": line}),
"hole_simple" => json!({"points": null, "to": p(4.0)}),
"hole_drilled" | "hole_counterbore" | "hole_countersink" => json!({"to": p(4.0)}),
"cylinder_references" => {
json!({"references": [{"edge": p(11.0), "distance": "cyl.reference1"}, {"edge": p(12.0), "distance": "cyl.reference2"}]})
}
"hole_references" => {
json!({"references": [{"edge": p(11.0), "distance": "hole.reference1"}, {"edge": p(12.0), "distance": "hole.reference2"}]})
}
@@ -73,6 +73,13 @@ cylinder
Radius = cyl.radius (Length)
Height = cyl.height (Length)
expressions: cyl.radius, cyl.height, cyl.x, cyl.y, cyl.z
cylinder_references
inputs:
Radius = cyl.radius (Length)
Height = cyl.height (Length)
Reference distance = cyl.reference1 (Length)
Reference distance = cyl.reference2 (Length)
expressions: cyl.radius, cyl.height, cyl.reference1, cyl.reference2, 0, 0, 0
sphere
inputs:
Radius = sph.radius (Length)
@@ -30,6 +30,9 @@ box
cylinder
moves: axis[0], axis[2], base[0], base[1], base[2]
placement: cyl.x, cyl.y, cyl.z
cylinder_references
moves: axis[0], axis[2], base[0], base[1], base[2], references[0].edge[0], references[0].edge[1], references[0].edge[2], references[1].edge[0], references[1].edge[1], references[1].edge[2]
placement: 0, 0, 0
sphere
moves: center[0], center[1], center[2]
placement: sph.x, sph.y, sph.z
@@ -29,6 +29,7 @@ chamfer
remap_bodies: body
box
cylinder
cylinder_references
sphere
torus
cone
@@ -0,0 +1,5 @@
//! A Cylinder can be placed by distances from up to two straight edges of the face it stands on
//! (`references`: each `{"edge": [x, y, z], "distance": "…"}`, the edges' persistent names in the
//! feature's `edge_names`, #6380), as a Single hole is: its base centre sits where those
//! distances hold, measured in the face's plane, and follows the edges when the part changes.
//! Older designs need no transformation: a cylinder without `references` stays at its `base`.
+8
View File
@@ -152,6 +152,14 @@ pub fn reference_edge_names(st: &ModelState, at: Vec3, edges: &[Vec3]) -> Vec<St
}
}
/// The persistent names of a cylinder's reference edges (#6380), on the body it stands on at
/// `base` (its coordinates are numbers when it has reference edges).
pub fn cylinder_edge_names(st: &ModelState, base: &crate::PointExpr, references: &[crate::HoleReference]) -> Vec<String> {
let [x, y, z] = base.clone().map(|c| c.parse::<f64>().unwrap_or(0.0));
let edges: Vec<Vec3> = references.iter().map(|r| r.edge.0).collect();
reference_edge_names(st, Vec3::new(x, y, z), &edges)
}
/// The (non-mesh) body whose surface is nearest `p`.
fn body_at(st: &ModelState, p: Vec3) -> Option<&ModelBody> {
st.bodies
+1 -1
View File
@@ -73,7 +73,6 @@ pub use assembly::{
mat_mul, rigid,
};
pub use document::*;
pub use eval::KeptResults;
pub use eval::{
EdgeRef, FeatureResult, Model, ModelBody, ModelState, PlacedBodies, RevolveExtent, SavedModel, SolvedSketch, ThreadInfo, curve_pick_error,
edge_names_for, edge_refs, face_point_names, face_point_state, hole_direction_at, import_body_names, map_wire, metric_thread_sizes,
@@ -81,6 +80,7 @@ pub use eval::{
sketch_line_plane, state_in_frame, state_in_frame_at, suggested_thread, tangent_face_chain, through_all_distance, world_state,
world_state_reusing,
};
pub use eval::{EvalCache, KeptResults};
pub use expr::Expr;
pub use names::{BodyName, FeatureId, FeatureName, SketchId, copy_name};
pub use params::{ParamRow, ParamUser, cycles};
+224
View File
@@ -0,0 +1,224 @@
//! Retain a proven evaluation across changes to assembly placement. Unknown feature kinds
//! use the ordinary evaluator; adding an eligible kind requires checking all of its inputs.
use super::{Document, Feature, FeatureKind, Model, ModelState, Operation, PlaneRef};
use std::sync::{Arc, Weak};
#[derive(Clone, Debug)]
pub(super) struct Inputs {
document: Arc<Document>,
state: Weak<ModelState>,
}
impl Model {
/// Evaluate a shared document, retaining a proof for subsequent placement-only changes.
/// Returns whether the timeline was evaluated. The normal `evaluate` remains available
/// for callers without a shared document and invalidates this proof.
///
/// Only explicitly supported local geometry qualifies. Geometry edits, unknown feature
/// kinds, stale/primed results and `no_cache` continue through the ordinary evaluator.
pub fn evaluate_shared(&mut self, doc: &Arc<Document>) -> bool {
let reusable = !self.no_cache
&& self.primed.is_none()
&& self.placement_inputs.as_ref().is_some_and(|inputs| {
inputs.state.upgrade().is_some_and(|state| Arc::ptr_eq(&state, &self.state()))
&& !self.is_stale()
&& (Arc::ptr_eq(doc, &inputs.document) || same_geometry_inputs(doc, &inputs.document))
});
if reusable {
self.last_recomputed = 0;
self.last_restored = 0;
return false;
}
self.evaluate(doc);
if !self.no_cache && self.primed.is_none() && self.placement_local {
self.placement_inputs = Some(Inputs { document: doc.clone(), state: Arc::downgrade(&self.state()) });
}
true
}
/// [`Model::refingerprint`] for a shared document: a proof that held for the results before
/// is kept for `doc`, since nothing was built differently (pinned body names, say).
pub fn refingerprint_shared(&mut self, doc: &Arc<Document>) {
let proven = !self.no_cache
&& self.primed.is_none()
&& self.placement_inputs.as_ref().is_some_and(|inputs| inputs.state.upgrade().is_some_and(|state| Arc::ptr_eq(&state, &self.state())))
&& !self.is_stale();
self.refingerprint(doc);
if proven && self.placement_local {
self.placement_inputs = Some(Inputs { document: doc.clone(), state: Arc::downgrade(&self.state()) });
}
}
}
fn local_plane(plane: &PlaneRef, depth: usize) -> bool {
if depth > crate::MAX_PLACEMENT_DEPTH {
return false;
}
match plane {
PlaneRef::Origin { name: _ } | PlaneRef::Custom { plane: _ } | PlaneRef::Placed { origin: _, x_dir: _, y_dir: _ } => true,
PlaneRef::Offset { base, distance: _ }
| PlaneRef::Flipped { base, about_y: _ }
| PlaneRef::AtAngle { base, axis_origin: _, axis_dir: _, angle: _, line: None } => local_plane(base, depth + 1),
_ => false,
}
}
pub(super) fn local_geometry(f: &Feature, components: &std::collections::HashMap<u64, u64>) -> bool {
// Persistent picked-face/edge attachments can move an otherwise literal input.
if !f.face_names.is_empty() || !f.edge_names.is_empty() || !f.edge_refs.is_empty() {
return false;
}
match &f.kind {
// A cylinder placed by reference edges follows the body it stands on.
FeatureKind::Cylinder { references, .. } if !references.is_empty() => false,
FeatureKind::Box { corner: _, length: _, width: _, height: _, operation: Operation::NewBody }
| FeatureKind::Cylinder { base: _, axis: _, radius: _, height: _, operation: Operation::NewBody, references: _ }
| FeatureKind::Sphere { center: _, radius: _, operation: Operation::NewBody }
| FeatureKind::Torus { center: _, major: _, minor: _, operation: Operation::NewBody }
| FeatureKind::Import { file: _, step: _, components: _, skip: _ }
| FeatureKind::MeshImport { file: _, meshes: _ } => true,
FeatureKind::Sketch { plane, sketch } => sketch.links.is_empty() && sketch.wires.is_empty() && local_plane(plane, 0),
FeatureKind::Extrude { sketch, profiles: _, extent, operation: Operation::NewBody, targets: _, thin: _ } => {
let crate::Extent {
distance: _,
convention: _,
direction: _,
distance2: _,
start_offset: _,
through_all,
taper: _,
to,
to_offset: _,
from,
} = extent;
to.is_none() && from.is_none() && !through_all && components.get(&sketch.0) == Some(&f.component)
}
_ => false,
}
}
/// Every non-placement field is named, so a new input requires an explicit decision here.
/// Assembly records do not feed any of the feature kinds accepted by `local_geometry`.
fn same_geometry_inputs(a: &Document, b: &Document) -> bool {
let Document {
render,
format,
format_pending,
name,
units,
metadata,
params,
features,
marker,
next_id,
reserved_body_names,
components,
occurrences: _,
assembly: _,
sheet,
plastic,
parts,
links,
configs,
favorites,
canvases,
param_comments,
text_params,
next_component_color_index,
materials,
user_materials,
appearances,
body_components,
body_offsets,
browser_groups,
timeline_groups,
browser_order,
named_views,
visibility,
} = a;
(Arc::ptr_eq(render, &b.render) || *render == b.render)
&& *format == b.format
&& *format_pending == b.format_pending
&& *name == b.name
&& *units == b.units
&& *metadata == b.metadata
&& *params == b.params
&& features.len() == b.features.len()
&& features.iter().zip(&b.features).all(|(a, b)| same_feature(a, b))
&& *marker == b.marker
&& *next_id == b.next_id
&& *reserved_body_names == b.reserved_body_names
&& *components == b.components
&& *sheet == b.sheet
&& *plastic == b.plastic
&& *parts == b.parts
&& *links == b.links
&& *configs == b.configs
&& *favorites == b.favorites
&& *canvases == b.canvases
&& *param_comments == b.param_comments
&& *text_params == b.text_params
&& *next_component_color_index == b.next_component_color_index
&& *materials == b.materials
&& *user_materials == b.user_materials
&& *appearances == b.appearances
&& *body_components == b.body_components
&& *body_offsets == b.body_offsets
&& *browser_groups == b.browser_groups
&& *timeline_groups == b.timeline_groups
&& *browser_order == b.browser_order
&& *named_views == b.named_views
&& *visibility == b.visibility
}
/// Compare feature definitions without traversing immutable imported payloads. A replacement
/// allocation conservatively evaluates again, even when it happens to contain equal bytes.
fn same_feature(a: &Feature, b: &Feature) -> bool {
let Feature {
id,
name,
suppressed,
body_names,
param_names,
component,
edge_refs,
edge_names,
face_names,
participants,
shell_piece_order,
kernel_piece_order,
inserted,
link_faces,
kind,
} = a;
*id == b.id
&& *name == b.name
&& *suppressed == b.suppressed
&& *body_names == b.body_names
&& *param_names == b.param_names
&& *component == b.component
&& *edge_refs == b.edge_refs
&& *edge_names == b.edge_names
&& *face_names == b.face_names
&& *participants == b.participants
&& *shell_piece_order == b.shell_piece_order
&& *kernel_piece_order == b.kernel_piece_order
&& *inserted == b.inserted
&& *link_faces == b.link_faces
&& match (kind, &b.kind) {
(FeatureKind::MeshImport { file: a, meshes }, FeatureKind::MeshImport { file: b, meshes: other }) => {
a == b
&& meshes.len() == other.len()
&& meshes.iter().zip(other).all(|(a, b)| {
let crate::MeshData { name, positions, triangles, color } = a;
*name == b.name && *color == b.color && Arc::ptr_eq(positions, &b.positions) && Arc::ptr_eq(triangles, &b.triangles)
})
}
(
FeatureKind::Import { file: a, step, components, skip },
FeatureKind::Import { file: b, step: other, components: other_components, skip: other_skip },
) => a == b && Arc::ptr_eq(step, other) && components == other_components && skip == other_skip,
(a, b) => a == b,
}
}
+16
View File
@@ -436,6 +436,7 @@ fn primitives_and_combine() {
radius: "2".into(),
height: "20".into(),
operation: Operation::NewBody,
references: Vec::new(),
},
None,
)
@@ -562,6 +563,21 @@ fn feature_inserted_at_a_marker_inside_a_timeline_folder_joins_it() {
assert!(Document::from_json(&doc.to_json()).is_ok(), "the inserted feature must leave a reopenable design");
}
/// A marker after the last feature leaves every feature active, as no marker does, so a feature
/// added then is active too, not added rolled back (#7741: editing the last feature from the
/// History runs its command with the marker there).
#[test]
fn feature_added_with_the_marker_at_the_end_is_active() {
let mut doc = Document::new("marker at the end");
let first = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: Sketch::new() }, Some("First")).unwrap();
doc.marker = Some(1);
let added = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: Sketch::new() }, Some("Added")).unwrap();
assert_eq!(doc.features.iter().map(|feature| feature.id).collect::<Vec<_>>(), vec![first, added]);
assert_eq!(doc.marker, Some(2), "the marker stays after the last feature");
}
/// A construction plane that is its own side resolves through the model and back into the
/// document. The depth count has to survive that hop, or the recursion overflows the stack and
/// aborts the process, which no panic guard can intercept.
+58
View File
@@ -199,6 +199,12 @@ fn is_construction(k: &FeatureKind) -> bool {
fn shown_item(s: &Session, key: &str) -> Option<Shown> {
if let Some(f) = s.doc.find_feature(key) {
if matches!(f.kind, FeatureKind::Sketch { .. }) {
// A sketch and a body with exactly the same name, a sketch named after the body it
// makes (#7206): the name is the body's. The browser names a sketch by its id and a
// body only by its name, so the body could not be reached otherwise.
if f.name == key && s.world_state().bodies.iter().any(|b| b.name == key) {
return Some(Shown::Body(key.to_string()));
}
return Some(Shown::Sketch(f.id));
}
if is_construction(&f.kind) {
@@ -1589,6 +1595,58 @@ mod tests {
}
}
/// #7206, #7569: a sketch named after the body it makes. The name hides and shows the body,
/// as the body's eye in the browser asks; the sketch, which the browser names by its id,
/// still hides and shows by its id, and the body is left alone.
#[test]
fn a_body_hides_by_its_name_when_a_sketch_has_the_same_name() {
let mut s = Session::default();
let sketch = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "timeline.rename", json!({"feature": sketch, "name": "Deckel"}));
run(&mut s, "solid.extrude", json!({"sketch": sketch, "distance": 5, "body_name": "Deckel"}));
let r = run(&mut s, "browser.visibility", json!({"items": ["Deckel"], "visible": false}));
assert_eq!(r["changed"], 1);
assert_eq!(s.visibility.hidden_bodies, vec!["Deckel".to_string()]);
assert!(s.visibility.hidden_sketches.is_empty(), "the sketch is not the item that changed");
run(&mut s, "browser.visibility", json!({"items": ["Deckel"], "visible": true}));
assert!(s.visibility.hidden_bodies.is_empty());
// The sketch by its id: shown and hidden again, the body stays shown.
run(&mut s, "browser.visibility", json!({"items": [sketch], "visible": true}));
assert_eq!(s.visibility.shown_sketches, vec![sketch]);
run(&mut s, "browser.visibility", json!({"items": [sketch], "visible": false}));
assert_eq!(s.visibility.hidden_sketches, vec![sketch]);
assert!(s.visibility.hidden_bodies.is_empty());
}
#[test]
fn a_sheet_metal_flange_hides_its_sketch_as_an_extrude_does() {
// #6804: the base flange's sketch stayed shown; its line on the plate's bottom edge then
// took the Flange dialog's click and made a flat contour wall with no bend.
let mut s = Session::default();
let base = run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-33.5, -37], "p1": [27.5, -7]}));
run(&mut s, "sketch.finish", json!({}));
let wall = run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "Wall"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [40, 30]], "ids": ["l1", "l2"]}));
run(&mut s, "sketch.finish", json!({}));
assert!(crate::view::sketch_shown(&s, base) && crate::view::sketch_shown(&s, wall));
run(&mut s, "sheet.flange", json!({"sketch": "Base"}));
assert!(!crate::view::sketch_shown(&s, base), "the base flange used its sketch");
run(&mut s, "sheet.flange", json!({"sketch": "Wall", "curves": ["l1", "l2"], "distance": 20}));
assert!(!crate::view::sketch_shown(&s, wall), "the contour flange used its sketch");
let d = run(&mut s, "document.inspect", json!({}));
assert!(d["sketches"].as_array().unwrap().iter().all(|x| x["visible"] == false), "{d}");
assert_eq!(s.doc.features.iter().filter(|f| matches!(f.kind, solidcraft_doc::FeatureKind::SheetContour { .. })).count(), 1);
// Shown on their own, or while their flange is edited, they are drawn again.
s.feature_edit_sketch = Some(base);
assert!(crate::view::sketch_shown(&s, base));
s.feature_edit_sketch = None;
run(&mut s, "browser.visibility", json!({"items": [wall], "visible": true}));
assert!(crate::view::sketch_shown(&s, wall) && !crate::view::sketch_shown(&s, base));
}
#[test]
fn helper_sketches_hide_and_show() {
let mut s = Session::default();
+121 -8
View File
@@ -8,7 +8,9 @@
//! the design gives them (`Feature::link_faces`), so a joint on a linked face stays on that face
//! when the design changes, as it would inside the design, and reports it when the face is gone.
use std::path::Path;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use serde_json::{Value, json};
use solidcraft_doc::{DesignLink, Document, FeatureKind};
@@ -88,20 +90,87 @@ pub fn resolve_design_links(doc: &mut Document, from: Option<&str>) {
/// the design gives their faces (`Feature::link_faces`), with the source's design properties.
type Linked = (String, solidcraft_io::StepFeature, Vec<Vec<String>>, solidcraft_doc::Metadata);
/// The solid bodies of the design at `file`, evaluated, as an Import feature.
fn linked_bodies(file: &Path, own: Option<&str>) -> Result<Linked> {
/// What a linked file last gave, by the file: digests of its bytes, of the STEP text it gave and
/// of its face names, and that read's warnings. Update Links evaluates a file again only when its
/// bytes or the linked bodies changed since (#5606). Bounded, oldest first out.
static READS: Mutex<Vec<LinkRead>> = Mutex::new(Vec::new());
const READS_SIZE: usize = 256;
struct LinkRead {
file: PathBuf,
bytes: u64,
step: u64,
faces: u64,
warnings: Vec<String>,
}
#[cfg(test)]
thread_local! {
/// Linked designs read and evaluated on this thread, for tests.
static EVALUATED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn digest<T: Hash + ?Sized>(value: &T) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
value.hash(&mut h);
h.finish()
}
/// The warnings of the last read of `file`, when it had these bytes and gave the STEP text
/// `step` and the face names `faces`: the bodies it gives are the ones the link holds.
fn unchanged_read(file: &Path, bytes: u64, step: u64, faces: u64) -> Option<Vec<String>> {
let file = absolute(file);
let reads = READS.lock().ok()?;
reads.iter().find(|r| r.file == file && r.bytes == bytes && r.step == step && r.faces == faces).map(|r| r.warnings.clone())
}
fn remember_read(file: &Path, bytes: u64, feature: &solidcraft_io::StepFeature, faces: &[Vec<String>]) {
let FeatureKind::Import { step, .. } = &feature.kind else { return };
let file = absolute(file);
let Ok(mut reads) = READS.lock() else { return };
reads.retain(|r| r.file != file);
if reads.len() >= READS_SIZE {
reads.remove(0);
}
reads.push(LinkRead { file, bytes, step: digest(step.as_bytes()), faces: digest(faces), warnings: feature.warnings.clone() });
}
/// The bytes of the design at `file`, which must not be the design saved at `own`.
fn read_link(file: &Path, own: Option<&str>) -> Result<Vec<u8>> {
let shown = file.display().to_string();
if own.is_some_and(|o| absolute(Path::new(o)) == absolute(file)) {
return Err(EngineError::Other(format!("{shown}: a design can't be linked into itself")));
}
// A network location only when its server is allowed, a device never: judged from the text
// before anything is read (on Windows, reading `\\host\share\x` connects to the host).
let bytes = solidcraft_io::vfs::read_design_path(&shown, own, solidcraft_io::MAX_DESIGN_BYTES).map_err(|e| {
solidcraft_io::vfs::read_design_path(&shown, own, solidcraft_io::MAX_DESIGN_BYTES).map_err(|e| {
let e = e.to_string();
EngineError::Other(if e.starts_with(&shown) { e } else { format!("{shown}: {e}") })
})?;
let doc = solidcraft_io::read_design(&bytes).map_err(|e| EngineError::Other(format!("{shown}: {e}")))?;
session_bodies(&Session::opened_at(doc, None, Some(&shown)), &shown)
})
}
/// The solid bodies of the design at `file`, evaluated, as an Import feature.
fn linked_bodies(file: &Path, own: Option<&str>) -> Result<Linked> {
design_bodies(file, &read_link(file, own)?)
}
/// The solid bodies of `bytes`, the design at `file`, as an Import feature. The model the file
/// keeps is its evaluation when this build saved it for exactly that design, as when it is opened.
fn design_bodies(file: &Path, bytes: &[u8]) -> Result<Linked> {
#[cfg(test)]
EVALUATED.with(|n| n.set(n.get() + 1));
let shown = file.display().to_string();
let fail = |e: solidcraft_io::IoError| EngineError::Other(format!("{shown}: {e}"));
let (doc, saved) = if solidcraft_io::is_design_container(bytes) {
let mut reader = solidcraft_io::DesignContainerReader::new(std::io::Cursor::new(bytes)).map_err(fail)?;
let saved = reader.geometry();
(reader.read_document().map_err(fail)?, saved)
} else {
(solidcraft_io::read_design(bytes).map_err(fail)?, None)
};
let (name, feature, faces, metadata) = session_bodies(&Session::opened_at(doc, saved.as_ref(), Some(&shown)), &shown)?;
remember_read(file, digest(bytes), &feature, &faces);
Ok((name, feature, faces, metadata))
}
/// The solid bodies of `part`, the design saved as `shown`, as an Import feature.
@@ -224,7 +293,14 @@ fn update_links(s: &mut Session, p: &Value) -> Result<Value> {
let old = old.clone();
link_refused(s.path.as_deref(), &link.path).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
let file = resolve(s.path.as_deref(), &link.path);
let (_, sf, faces, _) = linked_bodies(&file, s.path.as_deref()).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
let bytes = read_link(&file, s.path.as_deref()).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
let kept_faces = s.doc.feature(link.feature).map(|f| digest(f.link_faces.as_slice()));
if let Some(known) = kept_faces.and_then(|faces| unchanged_read(&file, digest(bytes.as_slice()), digest(old.as_bytes()), faces)) {
warnings.extend(known);
unchanged.push(cname);
continue;
}
let (_, sf, faces, _) = design_bodies(&file, &bytes).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
warnings.extend(sf.warnings.iter().cloned());
let same = matches!(&sf.kind, FeatureKind::Import { step, .. } if *step == old);
let kept = s.doc.feature(link.feature).map(|f| &f.link_faces);
@@ -525,6 +601,43 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn update_links_reads_again_only_the_designs_that_changed() {
let dir = std::env::temp_dir().join(format!("solidcraft-link-reads-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (plate, block) = (dir.join("plate.solidcraft"), dir.join("block.solidcraft"));
save_box(&plate, 30.0);
save_box(&block, 10.0);
let mut a = Session::default();
run(&mut a, "component.insert_design", json!({"path": plate}));
run(&mut a, "component.insert_design", json!({"path": block, "translate": [50, 0, 0]}));
let evaluated = || EVALUATED.with(std::cell::Cell::get);
// Unchanged files are not evaluated again.
let before = evaluated();
let r = run(&mut a, "component.update_links", json!({}));
assert_eq!((r["updated"].clone(), r["unchanged"].clone()), (json!([]), json!(["plate", "block"])), "{r}");
assert_eq!(evaluated(), before);
// An edited file is, and only that one.
save_box(&plate, 45.0);
let r = run(&mut a, "component.update_links", json!({}));
assert_eq!((r["updated"].clone(), r["unchanged"].clone()), (json!(["plate"]), json!(["block"])), "{r}");
assert_eq!(evaluated(), before + 1);
assert!((volume(&mut a) - 11000.0).abs() < 1e-6, "{}", volume(&mut a));
// Undone, the link holds the old bodies again: the same file brings the new ones back.
run(&mut a, "edit.undo", json!({}));
assert!((volume(&mut a) - 8000.0).abs() < 1e-6, "{}", volume(&mut a));
assert_eq!(run(&mut a, "component.update_links", json!({}))["updated"], json!(["plate"]));
assert!((volume(&mut a) - 11000.0).abs() < 1e-6, "{}", volume(&mut a));
// A file saved again without its model is evaluated, and gives the same bodies.
let mut part = Session::default();
run(&mut part, "doc.open", json!({"path": block}));
run(&mut part, "file.save", json!({"cache": false}));
let before = evaluated();
assert_eq!(run(&mut a, "component.update_links", json!({}))["unchanged"], json!(["plate", "block"]));
assert_eq!(evaluated(), before + 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn inserting_a_missing_file_or_bad_parameters_changes_nothing() {
let mut s = Session::default();
+66 -1
View File
@@ -160,7 +160,11 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, format!("no occurrence `{k}`")));
};
if s.doc.occurrences.iter().filter(|x| x.component == o.component).count() > 1 {
s.doc_mut().occurrences.retain(|x| x.id != o.id);
// The joints on it and the other assembly records naming it go with it: left behind,
// a joint stays in the Browser joined to nothing (#7749).
let gone = s.doc_mut().remove_occurrences(&std::collections::BTreeSet::from([o.id]));
s.visibility.hidden_joints.retain(|h| !gone.contains(h));
joints.extend(gone);
s.pending_moves.remove(&o.id);
done = true;
} else {
@@ -270,6 +274,43 @@ mod tests {
assert_eq!(done["would_fail"], json!(["Hole"]), "{done}");
}
/// Deleting a dimension of a sketch with a rolled-back history after it (#7253): the sketch
/// keeps its geometry, so the check before the delete does not build the features after the
/// marker again (that took minutes on a long history). A deleted line still breaks them, and
/// the check still says so.
#[test]
fn deleting_a_dimension_in_a_rolled_back_history_builds_nothing_after_the_marker() {
let mut s = Session::default();
let sk = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [30, 20], [10, 20]], "closed": true}));
let lines: Vec<String> = s.model.state().sketch(sk).unwrap().sketch.curves.iter().map(|c| c.id.clone()).collect();
let param = run(&mut s, "sketch.dimension", json!({"entities": [lines[0], lines[1]], "type": "angle"}))["param"].as_str().map(str::to_string);
let angle = s.model.state().sketch(sk).unwrap().sketch.constraints.iter().find(|c| param.is_some() && c.param == param).unwrap().id.clone();
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance": 5, "name": "Plate"}));
let later = 6;
for i in 0..later {
run(&mut s, "solid.cylinder", json!({"base": [12 + 3 * i, 8, -1], "radius": 1, "height": 10, "operation": "cut"}));
}
run(&mut s, "timeline.roll_to", json!({"position": 1}));
run(&mut s, "sketch.edit", json!({"sketch": sk}));
let cached = |s: &Session| s.model.cache.lock().map(|c| c.len()).unwrap_or(0);
let before = cached(&s);
let doc = s.doc.clone();
let items = json!([{"type": "sketch_constraint", "id": angle}]);
let dry = run(&mut s, "selection.delete", json!({"items": items, "dry_run": true}));
assert_eq!((&dry["deleted"], &dry["would_fail"]), (&json!([]), &json!([])), "{dry}");
assert_eq!(*s.doc, *doc, "the check changes nothing");
// The sketch itself is solved again; Plate and the cuts are not built.
let built = cached(&s).saturating_sub(before);
assert!(built < later, "{built} results built for the check");
let line = json!([{"type": "sketch_curve", "id": lines[1]}]);
let dry = run(&mut s, "selection.delete", json!({"items": line, "dry_run": true}));
assert!(dry["would_fail"].as_array().unwrap().contains(&json!("Plate")), "{dry}");
run(&mut s, "selection.delete", json!({"items": items}));
assert!(s.model.state().sketch(sk).unwrap().sketch.constraints.iter().all(|c| c.id != angle));
}
#[test]
fn mixed_selection_is_one_undo_step() {
let (mut s, _) = part();
@@ -406,6 +447,30 @@ mod tests {
}
}
/// An instance deleted while its component has others goes alone, and the joints on it go
/// with it, with their motion links (#7749): left behind, a joint names an occurrence that is
/// gone, and every solve reports it.
#[test]
fn an_instance_deleted_alone_takes_its_joints_with_it() {
let mut s = joint_assembly();
let b1 = s.doc.occurrences.iter().find(|o| o.name == "B:1").unwrap().id;
let b2 = run(&mut s, "occurrence.copy", json!({"occurrence": "B:1", "translate": [0, 40, 0]}))["occurrence"].as_u64().unwrap();
let dry = run(&mut s, "selection.delete", json!({"occurrences": [b1], "dry_run": true}));
assert_eq!(s.doc.assembly.joints.len(), 2, "a dry run changes nothing");
let deleted = run(&mut s, "selection.delete", json!({"occurrences": [b1]}));
assert_eq!(deleted, dry);
let left: Vec<&str> = s.doc.assembly.joints.iter().map(|j| j.name.as_str()).collect();
assert_eq!(left, ["Glue"], "the joint on the deleted instance goes with it");
assert!(s.doc.assembly.links.is_empty(), "and so does its motion link");
assert_eq!(deleted["deleted_joints"].as_array().map(Vec::len), Some(1), "the reply names it: {deleted}");
assert!(s.doc.occurrences.iter().any(|o| o.id == b2), "the other instance stays");
let solved = run(&mut s, "joint.solve", json!({}));
assert_eq!(solved["errors"], json!([]), "no joint is left pointing at the deleted instance");
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.assembly.joints.len(), 2);
assert_eq!(s.doc.assembly.links.len(), 1);
}
#[test]
fn occurrences_components_and_sketch_entities() {
let (mut s, _) = part();
+117 -14
View File
@@ -12,6 +12,14 @@ use crate::{EngineError, Result, Sel, Session, Snapshot};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("edit.undo", "Undo", undo).icon("undo").key("Ctrl+Z").noundo().doc_only(),
CommandSpec::new("edit.redo", "Redo", redo).icon("redo").key("Ctrl+Y").noundo().doc_only(),
CommandSpec::new("edit.repeat", "Repeat Last Command", crate::checked!("edit.repeat", repeat))
.icon("redo")
.key("Shift+R")
.noundo()
.doc_only()
.contract(&REPEAT_CONTRACT)
.output(crate::schema::output_schema_for::<crate::schema::edit::RepeatResult>)
.params("Starts the last tool or dialog started in the app again, with its defaults (a toolbar click, not a replay of its values). Only the app has a tool to start: called here it is refused, and changes nothing"),
CommandSpec::new("edit.checkpoint", "Checkpoint", checkpoint).icon("checkpoint")
.noundo()
.doc_only()
@@ -25,7 +33,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SOLID", "MODIFY")
.icon("delete")
.key("Delete")
.params("feature: id|name | features: [id|name] (one undo step; a sketch takes its dependent features with it) → deleted: [ids]; failing?: [{feature, error}] (later features that worked and now fail)"),
.params("feature: id|name | features: [id|name] | groups: [timeline folder ids] (a folder goes with all its features and nested folders; one undo step; a sketch takes its dependent features with it) → deleted: [ids]; groups?: [folder ids deleted]; failing?: [{feature, error}] (later features that worked and now fail)"),
CommandSpec::new("timeline.rename", "Rename", rename).icon("rename").params("feature: id|name, name (references to it follow)"),
CommandSpec::new("timeline.group", "New Feature Folder", group)
.icon("folder")
@@ -55,8 +63,9 @@ pub static COMMANDS: &[CommandSpec] = &[
.noundo()
.params("feature: id|name → features deleted with it, and features that would fail without it"),
CommandSpec::new("timeline.suppress", "Suppress Feature", suppress).icon("suppress").params(
"feature: id|name | features: [id|name] (one undo step), suppressed?: bool (default toggles: several features are all \
suppressed unless all of them already are, then all are unsuppressed) → feature: id (or features: [ids]), suppressed",
"feature: id|name | features: [id|name] | groups: [timeline folder ids] (every feature in those folders and their nested \
folders; one undo step), suppressed?: bool (default toggles: several features are all \
suppressed unless all of them already are, then all are unsuppressed) → feature: id (or features: [ids]; groups: [ids] when given), suppressed",
),
CommandSpec::new("timeline.edit", "Edit Feature", edit_feature).icon("edit").params(
"feature: id|name, set: {fields to change, merged into the feature's definition as timeline.inspect returns it, \
@@ -90,6 +99,16 @@ fn redo(s: &mut Session, _p: &Value) -> Result<Value> {
Ok(json!({"redone": label}))
}
const REPEAT_CONTRACT: crate::schema::TypedContract<crate::schema::edit::RepeatParams> = crate::schema::TypedContract::new();
/// Repeat Last Command (#7077) starts a tool, which only the app has: it starts the last one in
/// `SolveApp::start` and never reaches this. A script runs the command it wants itself.
fn repeat(_s: &mut Session, _p: &Value) -> Result<Value> {
Err(EngineError::Other(
"edit.repeat starts the last tool or dialog again in the app (its key, the canvas menu or command search); there is no tool to start here: run that command itself".into(),
))
}
/// Put the session at `snap`; returns where it was, under the same label.
fn go_to(s: &mut Session, snap: Snapshot) -> Snapshot {
let was = Snapshot {
@@ -162,9 +181,13 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
_ => String::new(),
});
}
if keys.is_empty() {
return Err(bad(cmd, "`features` must list features"));
let groups = group_ids(s, p, false, cmd)?;
if keys.is_empty() && groups.is_empty() {
return Err(bad(cmd, "`features` must list features, or `groups` timeline folders"));
}
// A folder goes with its features and nested folders, its features in timeline order.
let (folders, contents) = group_tree(&s.doc, &groups);
keys.extend(s.doc.features.iter().filter(|f| contents.contains(&f.id)).map(|f| f.id.to_string()));
// Features that work now; any of them failing after the delete is reported (a later
// feature that names a deleted body, #267).
let worked: std::collections::BTreeSet<u64> = s.model.results.iter().filter(|r| r.error.is_none() && !r.skipped).map(|r| r.id).collect();
@@ -178,6 +201,9 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
};
gone.extend(s.doc_mut().delete_feature(id)?);
}
if !folders.is_empty() {
s.doc_mut().timeline_groups.retain(|group| !folders.contains(&group.id));
}
if s.active_sketch.is_some_and(|a| gone.contains(&a)) {
s.active_sketch = None;
}
@@ -194,10 +220,14 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
.filter(|r| worked.contains(&r.id))
.filter_map(|r| r.error.as_ref().map(|e| json!({"feature": r.name, "error": e})))
.collect();
if failing.is_empty() {
return Ok(json!({"deleted": gone}));
let mut out = json!({"deleted": gone});
if !folders.is_empty() {
out["groups"] = json!(folders);
}
Ok(json!({"deleted": gone, "failing": failing}))
if !failing.is_empty() {
out["failing"] = json!(failing);
}
Ok(out)
}
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
@@ -312,6 +342,11 @@ fn group_move_would_cycle(s: &Session, target: u64, sources: &[u64]) -> bool {
}
fn group_feature_ids(doc: &solidcraft_doc::Document, roots: &[u64]) -> std::collections::BTreeSet<u64> {
group_tree(doc, roots).1
}
/// The timeline folders `roots` and every folder nested in them, and the features they hold.
fn group_tree(doc: &solidcraft_doc::Document, roots: &[u64]) -> (std::collections::BTreeSet<u64>, std::collections::BTreeSet<u64>) {
let by_id: std::collections::BTreeMap<u64, &solidcraft_doc::TimelineGroup> = doc.timeline_groups.iter().map(|group| (group.id, group)).collect();
let mut children = std::collections::BTreeMap::<u64, Vec<u64>>::new();
for group in &doc.timeline_groups {
@@ -331,7 +366,7 @@ fn group_feature_ids(doc: &solidcraft_doc::Document, roots: &[u64]) -> std::coll
pending.extend(children.get(&id).into_iter().flatten().copied());
}
}
features
(visited, features)
}
fn sort_group_features(doc: &mut solidcraft_doc::Document) {
@@ -620,7 +655,15 @@ fn counted_names_from(s: &Session, ids: &[u64]) -> Vec<(u64, Vec<String>)> {
fn suppress(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "timeline.suppress";
let ids = feature_ids(s, p, cmd)?;
let groups = group_ids(s, p, false, cmd)?;
let named = |key: &str| p.get(key).is_some_and(|v| !v.is_null());
let mut ids = if groups.is_empty() || named("feature") || named("features") { feature_ids(s, p, cmd)? } else { Vec::new() };
let contents = group_feature_ids(&s.doc, &groups);
let more: Vec<u64> = s.doc.features.iter().map(|f| f.id).filter(|id| contents.contains(id) && !ids.contains(id)).collect();
ids.extend(more);
if ids.is_empty() {
return Err(bad(cmd, "the timeline folders hold no features"));
}
let want = match bool_(p, "suppressed") {
Some(b) => b,
None if p.get("suppressed").is_some_and(|v| !v.is_null()) => return Err(bad(cmd, "`suppressed` must be true or false")),
@@ -640,8 +683,9 @@ fn suppress(s: &mut Session, p: &Value) -> Result<Value> {
f.suppressed = want;
}
match ids.as_slice() {
[id] if p.get("features").is_none_or(Value::is_null) => Ok(json!({"feature": id, "suppressed": want})),
_ => Ok(json!({"features": ids, "suppressed": want})),
[id] if groups.is_empty() && p.get("features").is_none_or(Value::is_null) => Ok(json!({"feature": id, "suppressed": want})),
_ if groups.is_empty() => Ok(json!({"features": ids, "suppressed": want})),
_ => Ok(json!({"features": ids, "groups": groups, "suppressed": want})),
}
}
@@ -1099,6 +1143,13 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
nf.edge_names = solidcraft_doc::hole_reference::reference_edge_names(&s.model.state_before(id), position.0, &edges(a));
}
}
// New reference edges for a cylinder (#6380): likewise.
if let (FeatureKind::Cylinder { references: a, base, .. }, FeatureKind::Cylinder { references: b, .. }) = (&nf.kind, &f.kind) {
let edges = |r: &[solidcraft_doc::HoleReference]| r.iter().map(|x| x.edge.0).collect::<Vec<_>>();
if edges(a) != edges(b) {
nf.edge_names = solidcraft_doc::hole_reference::cylinder_edge_names(&s.model.state_before(id), base, a);
}
}
// New bend edges for a conversion: the names of the body's edges there now.
if let (FeatureKind::SheetConvert { bends: a, body, .. }, FeatureKind::SheetConvert { bends: b, body: was, .. }) = (&nf.kind, &f.kind)
&& (a != b || body != was)
@@ -1171,14 +1222,15 @@ fn newly_broken(before: &solidcraft_doc::Model, after: &solidcraft_doc::Model) -
/// (the model of `after_doc`). With the history rolled back, the features after the marker are
/// evaluated too, on copies of the models (which share their cache): a delete made in the
/// rolled-back history that breaks them says so before it is made, rather than leaving error marks
/// that only show at the end of the timeline (#3471).
/// that only show at the end of the timeline (#3471). A change that leaves them the same inputs
/// needs no such evaluation ([`only_constraints_changed`]).
pub(super) fn newly_broken_in_history(
before_doc: &solidcraft_doc::Document,
before: &solidcraft_doc::Model,
after_doc: &solidcraft_doc::Document,
after: &solidcraft_doc::Model,
) -> Vec<(String, String)> {
if before_doc.marker.is_none() && after_doc.marker.is_none() {
if (before_doc.marker.is_none() && after_doc.marker.is_none()) || only_constraints_changed(before_doc, before, after_doc, after) {
return newly_broken(before, after);
}
let whole = |doc: &solidcraft_doc::Document, model: &solidcraft_doc::Model| {
@@ -1191,6 +1243,50 @@ pub(super) fn newly_broken_in_history(
newly_broken(&whole(before_doc, before), &whole(after_doc, after))
}
/// Whether `after_doc` differs from `before_doc` only in sketch constraints and dimensions (and
/// model parameters nothing uses any more), every sketch whose constraints changed solving to
/// the same geometry, profiles and ends as before. The features after the marker are then built
/// from the same inputs, so evaluating the whole history twice would find nothing new (#7253):
/// deleting one dimension of a sketch with a long rolled-back history after it took minutes.
fn only_constraints_changed(
before_doc: &solidcraft_doc::Document,
before: &solidcraft_doc::Model,
after_doc: &solidcraft_doc::Document,
after: &solidcraft_doc::Model,
) -> bool {
let without_constraints = |doc: &solidcraft_doc::Document| {
let mut doc = doc.clone();
for f in &mut doc.features {
if let FeatureKind::Sketch { sketch, .. } = &mut f.kind {
sketch.constraints.clear();
}
}
doc
};
let (mut b, a) = (without_constraints(before_doc), without_constraints(after_doc));
if b.params.iter().any(|p| !a.params.contains(p) && (!p.model || !after_doc.param_users(&p.name).is_empty())) {
return false;
}
b.params.retain(|p| a.params.contains(p));
if b != a {
return false;
}
// What a later feature reads of a sketch: the solved geometry, its plane, profiles and ends.
let solved = |model: &solidcraft_doc::Model, id: u64| {
let r = model.result(id).filter(|r| !r.skipped && r.error.is_none())?;
let s = r.state.sketch(id)?;
let mut sketch = s.sketch.clone();
sketch.constraints.clear();
Some((s.plane, sketch, serde_json::to_value(&s.profiles).ok()?, s.ends.clone(), s.report.ok()))
};
before_doc.features.iter().zip(&after_doc.features).all(|(fb, fa)| match (&fb.kind, &fa.kind) {
(FeatureKind::Sketch { sketch: sb, .. }, FeatureKind::Sketch { sketch: sa, .. }) if sb.constraints != sa.constraints => {
solved(before, fb.id).is_some_and(|x| solved(after, fa.id).is_some_and(|y| x == y))
}
_ => true,
})
}
fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "timeline.reorder";
let id = feature_id(s, p.get("feature"), cmd)?;
@@ -1440,6 +1536,11 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
// Run the constructor on a scratch session: failed edits must not disturb sketch mode,
// selections, component authoring state or the original feature's definition.
let mut trial = s.scratch();
// What the trial computes is kept for the session's own evaluation at the end: the cache may
// hold only the last few states of a large design, which would be computed twice.
let kept = s.model.kept();
let shared = kept.clone().unwrap_or_default();
trial.model.keep(Some(shared.clone()));
trial.active_component = old.component;
if let FeatureKind::Emboss { face: Some(face), .. } = &old.kind {
trial.active_occurrence_path = (!face.author_path.is_empty()).then(|| face.author_path.clone());
@@ -1549,7 +1650,9 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
}
s.pending_moves = trial.pending_moves;
s.doc = trial.doc;
s.model.keep(Some(shared));
s.refresh();
s.model.keep(kept);
if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) {
return Err(EngineError::Other(e));
}
+159
View File
@@ -40,6 +40,45 @@ fn roll_to_takes_a_number_or_end_and_refuses_anything_else() {
}
}
/// #7077: Repeat Last Command is a command (its key can change, command search finds it), but
/// only the app has a tool to start, so a script's call is refused and changes nothing.
#[test]
fn repeat_is_refused_outside_the_app_and_changes_nothing() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
let (doc, undo) = (s.doc.to_json(), s.undo.len());
let e = s.execute("edit.repeat", &json!({})).unwrap_err().to_string();
assert!(e.contains("in the app"), "{e}");
assert_eq!((s.doc.to_json(), s.undo.len()), (doc, undo));
let spec = crate::find_command("edit.repeat").unwrap();
assert_eq!((spec.shortcut, spec.tab, spec.undoable), (Some("Shift+R"), "", false));
}
#[test]
fn creating_after_deleting_future_features_keeps_the_new_feature_active() {
let mut s = Session::default();
for (name, corner, size) in [("Base", [0, 0, 0], 10), ("Later1", [20, 0, 0], 2), ("Later2", [40, 0, 0], 3)] {
run(&mut s, "solid.box", json!({"name": name, "corner": corner, "length": size, "width": size, "height": size, "operation": "new"}));
}
run(&mut s, "timeline.roll_to", json!({"position": 1}));
run(&mut s, "solid.box", json!({"name": "Inserted", "corner": [60, 0, 0], "length": 4, "width": 4, "height": 4, "operation": "new"}));
assert_eq!(s.doc.features.iter().map(|feature| feature.name.as_str()).collect::<Vec<_>>(), ["Base", "Inserted", "Later1", "Later2"]);
assert_eq!(s.doc.marker, Some(2), "with later features, the new feature is inserted at the cursor");
run(&mut s, "timeline.delete", json!({"features": ["Later1", "Later2"]}));
assert_eq!(s.doc.features.len(), 2);
assert_eq!(s.doc.marker, Some(2), "the cursor is at the end after deleting all later features");
run(&mut s, "solid.box", json!({"name": "Appended", "corner": [80, 0, 0], "length": 5, "width": 5, "height": 5, "operation": "new"}));
assert_eq!(s.doc.features.iter().map(|feature| feature.name.as_str()).collect::<Vec<_>>(), ["Base", "Inserted", "Appended"]);
assert_eq!(s.doc.marker, Some(3), "the new feature advances the cursor to the end");
assert_eq!(s.model.state().bodies.len(), 3, "the appended feature is evaluated, not left in the future");
let volume = run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume - 1189.0).abs() < 1e-6, "{volume}");
}
#[test]
fn g2_fillet_survives_edit_undo_and_reopen() {
use solidcraft_doc::{Document, FeatureKind, FilletStyle};
@@ -144,6 +183,24 @@ fn redefining_a_blend_replaces_persistent_edge_selection_and_survives_resize() {
assert!(s.model.result(old.id).unwrap().warning.is_none());
}
/// A redefine computes the features after the edited one once: its trial's results are kept
/// for the session's own evaluation, though the cache holds only the last state (a large
/// design's).
#[test]
fn redefining_computes_the_later_features_once() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 5, "name": "Base"}));
for x in [30, 40, 50] {
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [x, 0, 0]}));
}
*s.model.cache.lock().unwrap() = solidcraft_doc::EvalCache::with_budget(1);
let reply =
run(&mut s, "timeline.redefine", json!({"feature": "Base", "command": "solid.box", "params": {"length": 25, "width": 10, "height": 5}}));
assert_eq!(reply["recomputed"], 0, "{reply}");
let volume = run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume - (25.0 * 10.0 * 5.0 + 3.0 * 125.0)).abs() < 1e-6, "{volume}");
}
#[test]
fn redefining_preserves_body_names_participants_and_marker_without_temporary_features() {
let mut s = Session::default();
@@ -662,6 +719,108 @@ fn timeline_folders_can_nest_and_reject_cycles() {
assert_eq!(s.doc.timeline_groups.iter().find(|group| group.id == child).unwrap().parent, None, "deleting a folder promotes its children");
}
/// Four separate boxes, `First`…`Fourth`, for the timeline folder tests.
fn four_boxes() -> Session {
let mut s = Session::default();
for (i, name) in ["First", "Second", "Third", "Fourth"].into_iter().enumerate() {
let corner = [20 * i, 0, 0];
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": corner, "operation": "new_body", "name": name}));
}
s
}
fn feature_names(s: &Session) -> Vec<String> {
s.doc.features.iter().map(|feature| feature.name.clone()).collect()
}
/// `timeline.delete {groups}` deletes a folder with its features and nested folders in one undo
/// step; other folders and features stay, and `timeline.ungroup` still keeps the features (#7219).
#[test]
fn timeline_delete_takes_a_folder_with_its_features_and_nested_folders() {
let mut s = four_boxes();
let outer = run(&mut s, "timeline.group", json!({"name": "Outer", "features": ["Second"]}))["group"].as_u64().unwrap();
let inner = run(&mut s, "timeline.group", json!({"name": "Inner", "features": ["Third"], "parent": outer}))["group"].as_u64().unwrap();
let keep = run(&mut s, "timeline.group", json!({"name": "Keep", "features": ["Fourth"]}))["group"].as_u64().unwrap();
let before = s.doc.clone();
let out = run(&mut s, "timeline.delete", json!({"groups": [outer]}));
assert_eq!(feature_names(&s), ["First", "Fourth"]);
assert_eq!(out["groups"], json!([outer, inner]), "{out}");
assert_eq!(s.doc.timeline_groups.iter().map(|group| group.id).collect::<Vec<_>>(), [keep], "the nested folder goes too");
assert_eq!(s.model.state().bodies.len(), 2);
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(reopened.timeline_groups, s.doc.timeline_groups);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.features, before.features, "one undo step brings the features back");
assert_eq!(s.doc.timeline_groups, before.timeline_groups, "and the folders, nested as they were");
run(&mut s, "edit.redo", json!({}));
assert_eq!(feature_names(&s), ["First", "Fourth"]);
run(&mut s, "edit.undo", json!({}));
// A folder and a feature outside it in one delete; an empty folder goes on its own.
run(&mut s, "timeline.delete", json!({"groups": [inner], "features": ["First"]}));
assert_eq!(feature_names(&s), ["Second", "Fourth"]);
assert_eq!(s.doc.timeline_groups.iter().map(|group| group.name.as_str()).collect::<Vec<_>>(), ["Outer", "Keep"]);
let empty = run(&mut s, "timeline.group", json!({"name": "Empty"}))["group"].as_u64().unwrap();
assert_eq!(run(&mut s, "timeline.delete", json!({"groups": [empty]}))["deleted"], json!([]));
assert!(!s.doc.timeline_groups.iter().any(|group| group.id == empty));
let before = s.doc.clone();
for bad in [json!({"groups": [999_999]}), json!({"groups": ["Outer"]}), json!({"groups": 3}), json!({"groups": []})] {
assert!(s.execute("timeline.delete", &bad).is_err(), "{bad}");
assert_eq!(s.doc, before, "{bad}: a refused delete changes nothing");
}
}
/// `timeline.suppress {groups}` suppresses every feature of a folder and its nested folders in
/// one undo step, toggles like several features do, and works with the marker inside the
/// folder (#7219).
#[test]
fn timeline_suppress_takes_a_folder_with_its_nested_folders() {
let mut s = four_boxes();
let outer = run(&mut s, "timeline.group", json!({"name": "Outer", "features": ["Second"]}))["group"].as_u64().unwrap();
run(&mut s, "timeline.group", json!({"name": "Inner", "features": ["Third"], "parent": outer}));
let suppressed = |s: &Session| s.doc.features.iter().filter(|feature| feature.suppressed).map(|feature| feature.name.clone()).collect::<Vec<_>>();
let out = run(&mut s, "timeline.suppress", json!({"groups": [outer]}));
assert_eq!(out["suppressed"], true);
assert_eq!(out["groups"], json!([outer]));
assert_eq!(suppressed(&s), ["Second", "Third"]);
assert_eq!(s.model.state().bodies.len(), 2);
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(reopened.features, s.doc.features);
run(&mut s, "edit.undo", json!({}));
assert!(suppressed(&s).is_empty(), "one undo step");
run(&mut s, "edit.redo", json!({}));
assert_eq!(suppressed(&s), ["Second", "Third"]);
// Toggle: all suppressed, so all come back; with one of them active, all are suppressed.
run(&mut s, "timeline.suppress", json!({"groups": [outer]}));
assert!(suppressed(&s).is_empty());
run(&mut s, "timeline.suppress", json!({"feature": "Third"}));
run(&mut s, "timeline.suppress", json!({"groups": [outer]}));
assert_eq!(suppressed(&s), ["Second", "Third"]);
run(&mut s, "timeline.suppress", json!({"groups": [outer], "suppressed": false, "features": ["Fourth"]}));
assert!(suppressed(&s).is_empty(), "an explicit state, with a feature outside the folder as well");
// The marker between the folder's features: the folder is suppressed all the same, and the
// marker stays where it was.
run(&mut s, "timeline.roll_to", json!({"feature": "Second"}));
assert_eq!(s.doc.marker, Some(2));
run(&mut s, "timeline.suppress", json!({"groups": [outer], "suppressed": true}));
assert_eq!(suppressed(&s), ["Second", "Third"]);
assert_eq!(s.doc.marker, Some(2));
assert_eq!(s.model.state().bodies.len(), 1, "only First is before the marker and not suppressed");
let empty = run(&mut s, "timeline.group", json!({"name": "Empty"}))["group"].as_u64().unwrap();
let before = s.doc.clone();
for bad in [json!({"groups": [empty]}), json!({"groups": [999_999]}), json!({"groups": [outer], "suppressed": "yes"})] {
assert!(s.execute("timeline.suppress", &bad).is_err(), "{bad}");
assert_eq!(s.doc, before, "{bad}: a refused suppress changes nothing");
}
}
/// A field the feature's definition doesn't have is refused with the fields it does have,
/// instead of reporting success and changing nothing (#1491).
#[test]
+105 -30
View File
@@ -11,12 +11,21 @@ use solidcraft_geom::Vec3;
use super::{CommandSpec, not_in_sketch};
use crate::params::{bad, bool_, expr, expr_problem, req_expr, str_, string_list, vec2, vec3};
use crate::schema::features::{BoxParams, ExtrudeParams, FeatureResult};
use crate::schema::features::{
BoundingSolidParams, BoxParams, ConeParams, CylinderParams, ExtrudeParams, FeatureResult, PipeParams, ScaleParams, SphereParams, TorusParams,
};
use crate::schema::{TypedContract, output_schema_for};
use crate::{EngineError, Result, Session};
const BOX_CONTRACT: TypedContract<BoxParams> = TypedContract::new();
const EXTRUDE_CONTRACT: TypedContract<ExtrudeParams> = TypedContract::new();
const CYLINDER_CONTRACT: TypedContract<CylinderParams> = TypedContract::new();
const SPHERE_CONTRACT: TypedContract<SphereParams> = TypedContract::new();
const TORUS_CONTRACT: TypedContract<TorusParams> = TypedContract::new();
const CONE_CONTRACT: TypedContract<ConeParams> = TypedContract::new();
const PIPE_CONTRACT: TypedContract<PipeParams> = TypedContract::new();
const BOUNDING_SOLID_CONTRACT: TypedContract<BoundingSolidParams> = TypedContract::new();
const SCALE_CONTRACT: TypedContract<ScaleParams> = TypedContract::new();
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.extrude", "Extrude", crate::checked!("solid.extrude", extrude))
@@ -29,7 +38,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.revolve", "Revolve", revolve)
.at("SOLID", "CREATE")
.icon("revolve")
.params("axis: sketch line id | x | y (sketch axes) | X|Y|Z (world) | construction axis name (followed when edited) | {origin, dir} | {edge|face: [x,y,z]} (a straight or circular edge, or a cylinder, cone or torus face: about its axis, followed as the model changes); angle?: expr (default 360 deg); angle2?: expr (Two Sides: the other side's angle); direction?: symmetric (angle split either side, independent of axis direction); convention?: sketch_normal|legacy_axis_start (default sketch_normal; legacy retains older angular starts); to?: [x,y,z] (To Object: turn until that face, vertex or point); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile) and face_body?: name (whose face, where faces of several bodies meet), operation?, targets?, name?, body_name?"),
.params("axis: sketch line id | x | y (sketch axes) | X|Y|Z (world) | {type: world, axis: X|Y|Z} (as returned by timeline.inspect) | construction axis name (followed when edited) | {origin, dir} | {edge|face: [x,y,z]} (a straight or circular edge, or a cylinder, cone or torus face: about its axis, followed as the model changes); angle?: expr (default 360 deg); angle2?: expr (Two Sides: the other side's angle); direction?: symmetric (angle split either side, independent of axis direction); convention?: sketch_normal|legacy_axis_start (default sketch_normal; legacy retains older angular starts); to?: [x,y,z] (To Object: turn until that face, vertex or point); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile) and face_body?: name (whose face, where faces of several bodies meet), operation?, targets?, name?, body_name?"),
CommandSpec::new("solid.sweep", "Sweep", sweep)
.at("SOLID", "CREATE")
.icon("sweep")
@@ -41,10 +50,30 @@ pub static COMMANDS: &[CommandSpec] = &[
.contract(&BOX_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.cylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z] (centre of the bottom: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.sphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.torus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.cone", "Cone", prim_cone).at("SOLID", "CREATE").icon("cone").params("radius | diameter, height: expr (the base circle, and the top's distance along the axis; a negative height goes the other way); top_radius? | top_diameter?: expr (the top circle; default 0: a full cone ending in an apex); base?: [x,y,z] (centre of the base circle: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.cylinder", "Cylinder", crate::checked!("solid.cylinder", prim_cylinder))
.at("SOLID", "CREATE")
.icon("cylinder")
.contract(&CYLINDER_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("radius | diameter, height: expr; base?: [x,y,z] (centre of the bottom: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); references?: [{edge: [x,y,z] on a straight edge of the face it stands on, distance: expr}] (at most two, as solid.hole's: the base centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric base only); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.sphere", "Sphere", crate::checked!("solid.sphere", prim_sphere))
.at("SOLID", "CREATE")
.icon("sphere")
.contract(&SPHERE_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("radius | diameter: expr; center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.torus", "Torus", crate::checked!("solid.torus", prim_torus))
.at("SOLID", "CREATE")
.icon("torus")
.contract(&TORUS_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("major, minor: expr (radii); center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.cone", "Cone", crate::checked!("solid.cone", prim_cone))
.at("SOLID", "CREATE")
.icon("cone")
.contract(&CONE_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("radius | diameter, height: expr (the base circle, and the top's distance along the axis; a negative height goes the other way); top_radius? | top_diameter?: expr (the top circle; default 0: a full cone ending in an apex); base?: [x,y,z] (centre of the base circle: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?"),
CommandSpec::new("solid.fillet", "Fillet", fillet)
.at("SOLID", "MODIFY")
.icon("fillet")
@@ -70,17 +99,23 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("hole")
.key("H")
.params("position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); single?: bool (with one sketch point: a Single hole that follows the point, drilled into the face there); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points unless single); diameter | thread (a tapped hole: metric major minus pitch or tabulated UNC/UNF, G or Rp cut-tap drill) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); references?: [{edge: [x,y,z] on a straight edge of the face, distance: expr}] (at most two; the centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric position only); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" or \"1/4-20 UNC\" (cosmetic); handedness?: right|left (default right, needs thread for left); thread_depth?: positive length independent of drill depth (default: full useful cylindrical wall; must fit that wall); targets?: [body] (the bodies to drill; holes at the points of a sketch on a body's face default to that body, found along the holes' direction on it alone; explicit targets or participants override, [\"*\"] every body)"),
CommandSpec::new("solid.pipe", "Pipe", pipe)
CommandSpec::new("solid.pipe", "Pipe", crate::checked!("solid.pipe", pipe))
.at("SOLID", "CREATE")
.icon("pipe")
.contract(&PIPE_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("path_sketch: sketch; path: [curve ids in order]; diameter: expr; wall?: expr (hollow); operation?, targets?, name?, body_name?"),
CommandSpec::new("solid.bounding_solid", "Bounding Solid", bounding_solid)
CommandSpec::new("solid.bounding_solid", "Bounding Solid", crate::checked!("solid.bounding_solid", bounding_solid))
.at("SOLID", "CREATE")
.icon("bounding_solid")
.contract(&BOUNDING_SOLID_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("bodies?: [names] (default: all); margin?: expr; name?, body_name?"),
CommandSpec::new("solid.scale", "Scale", scale)
CommandSpec::new("solid.scale", "Scale", crate::checked!("solid.scale", scale))
.at("SOLID", "MODIFY")
.icon("scale")
.contract(&SCALE_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params("bodies: [names]; factor: expr | factors: [x, y, z] exprs; origin?: [x,y,z]"),
CommandSpec::new("face.delete", "Delete Face", delete_face)
.at("SOLID", "MODIFY")
@@ -517,6 +552,12 @@ pub(crate) fn profiles(p: &Value, cmd: &str) -> Result<ProfileSel> {
if fields.get("type").and_then(Value::as_str) == Some("whole_boundaries") {
return solidcraft_doc::whole_profile_selection_from_value(v).map_err(|error| bad(cmd, error.to_string()));
}
if !fields.contains_key("offset_boundaries") && fields.get("type").and_then(Value::as_str) != Some("offset_boundaries") {
return Err(bad(
cmd,
r#"`profiles` must be `"all"`, an index such as `0`, indices such as `[0]`, point picks such as `[{"point": [0, 0]}]` or curve-id picks such as `[{"curves": ["c1"]}]`"#,
));
}
let associations = fields
.get("offset_boundaries")
.or_else(|| (fields.get("type").and_then(Value::as_str) == Some("offset_boundaries")).then(|| fields.get("associations")).flatten())
@@ -639,6 +680,10 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
let edges: Vec<Vec3> = references.iter().map(|r| r.edge.0).collect();
(Vec::new(), solidcraft_doc::hole_reference::reference_edge_names(&s.model.state(), position.0, &edges))
}
// A cylinder's reference edges (#6380) are edges of the body it stands on.
FeatureKind::Cylinder { base, references, .. } if !references.is_empty() => {
(Vec::new(), solidcraft_doc::hole_reference::cylinder_edge_names(&s.model.state(), base, references))
}
// A conversion's bend edges are edges of the body it converts.
FeatureKind::SheetConvert { body, bends, .. } if !bends.is_empty() => {
(Vec::new(), solidcraft_doc::sheet::bend_edge_names(&s.model.state(), body, &solidcraft_doc::plain(bends)))
@@ -1201,7 +1246,8 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
}
/// A revolve axis of `sketch` from its parameter: a sketch line id, the sketch's x or y axis, a
/// world axis, a construction axis by name (followed when it is edited), {origin, dir}, or an edge
/// world axis (also as saved: {type: world, axis: X|Y|Z}), a construction axis by name
/// (followed when it is edited), {origin, dir}, or an edge
/// or round face of the model ({edge|face: [x,y,z]}), which the revolve follows as the model
/// changes. A line of the sketch wins over a construction axis of the same name.
pub(super) fn axis_param(s: &Session, sketch: u64, p: &Value, cmd: &str) -> Result<AxisRef> {
@@ -1209,6 +1255,11 @@ pub(super) fn axis_param(s: &Session, sketch: u64, p: &Value, cmd: &str) -> Resu
return Ok(AxisRef::Model { geo });
}
if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
if str_(o, "type") == Some("world") {
let axis =
str_(o, "axis").filter(|a| matches!(*a, "X" | "Y" | "Z")).ok_or_else(|| bad(cmd, "world axis requires `axis` to be X, Y or Z"))?;
return Ok(AxisRef::World { axis: axis.into() });
}
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
return Ok(AxisRef::Line { origin: Position(origin), dir: Dir(dir) });
@@ -1290,13 +1341,21 @@ pub(super) fn point_param(s: &Session, p: &Value, key: &str, cmd: &str) -> Resul
pub(super) fn axis_dir_param(p: &Value, cmd: &str) -> Result<Vec3> {
match p.get("axis") {
None => Ok(Vec3::Z),
Some(Value::String(a)) => match a.trim().to_ascii_uppercase().as_str() {
Some(v) => axis_dir_form(v).map_err(|why| bad(cmd, why)),
}
}
/// What [`axis_dir_param`] reads a given `axis` as, without the design: X, Y or Z (trimmed, any
/// case), else a non-zero `[x, y, z]`, normalized.
pub(crate) fn axis_dir_form(v: &Value) -> std::result::Result<Vec3, &'static str> {
match v {
Value::String(a) => match a.trim().to_ascii_uppercase().as_str() {
"X" => Ok(Vec3::X),
"Y" => Ok(Vec3::Y),
"Z" => Ok(Vec3::Z),
_ => Err(bad(cmd, "`axis` must be X, Y, Z or [x, y, z]")),
_ => Err("`axis` must be X, Y, Z or [x, y, z]"),
},
Some(v) => vec3(v).and_then(|a| a.normalized()).ok_or_else(|| bad(cmd, "`axis` must be X, Y, Z or a non-zero [x, y, z]")),
v => vec3(v).and_then(|a| a.normalized()).ok_or("`axis` must be X, Y, Z or a non-zero [x, y, z]"),
}
}
@@ -1337,7 +1396,19 @@ fn prim_cylinder(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, &height, Kind::Length, cmd, "height")?;
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
let axis = axis_dir_param(p, cmd)?;
add_feature(s, p, FeatureKind::Cylinder { base, axis: Dir(axis), radius, height, operation: operation(p, cmd)? })
// Reference edges (#6380), as a Single hole's: the base centre sits at these distances from
// straight edges of the face it stands on, and `base` keeps where those put it.
let references = hole_reference_params(s, p, cmd)?;
let base = if references.is_empty() {
base
} else {
let at = p
.get("base")
.and_then(vec3)
.ok_or_else(|| bad(cmd, "`references` place a cylinder by its `base` given as numbers, not length expressions"))?;
point_expr(referenced_centre(s, cmd, &references, at, -axis)?)
};
add_feature(s, p, FeatureKind::Cylinder { base, axis: Dir(axis), radius, height, operation: operation(p, cmd)?, references })
}
fn prim_sphere(s: &mut Session, p: &Value) -> Result<Value> {
@@ -2451,22 +2522,7 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
};
// Placed by its reference edges: drilled where their distances put it, which `position`
// then keeps. The edges must be straight edges at the given points, and cross.
let position = if references.is_empty() {
position
} else {
let st = s.model.state();
let mut distances = Vec::with_capacity(references.len());
for r in &references {
distances.push(s.doc.eval(&r.distance, Kind::Length).map_err(|e| bad(cmd, format!("`references`: {e}")))?);
}
let edges: Vec<(Vec3, Option<&str>)> = references.iter().map(|r| (r.edge.0, None)).collect();
let frame = solidcraft_doc::hole_reference::reference_frame(&st, position, direction, &edges, true).map_err(|e| bad(cmd, e.to_string()))?;
let centre = frame.place(position, &distances).map_err(|e| bad(cmd, e.to_string()))?;
if !solidcraft_doc::hole_reference::on_reference_face(&st, &frame, centre) {
return Err(bad(cmd, "the reference distances put the hole's centre off its face"));
}
centre
};
let position = if references.is_empty() { position } else { referenced_centre(s, cmd, &references, position, direction)? };
// The diameter: given, or a metric-estimate/Unified-table tap drill, or an ISO 273
// clearance hole for a bolt size (close, normal or loose fit).
let diameter = match (expr(p, "diameter"), &thread, str_(p, "clearance")) {
@@ -2551,6 +2607,25 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
)
}
/// Where reference edges (#37) put a hole's (or a cylinder's) centre picked at `at` on a flat
/// face, `dir` into it: at their distances from the edges, which must be straight edges at the
/// given points, and cross.
fn referenced_centre(s: &Session, cmd: &str, references: &[solidcraft_doc::HoleReference], at: Vec3, dir: Vec3) -> Result<Vec3> {
let st = s.model.state();
let mut distances = Vec::with_capacity(references.len());
for r in references {
distances.push(s.doc.eval(&r.distance, Kind::Length).map_err(|e| bad(cmd, format!("`references`: {e}")))?);
}
let edges: Vec<(Vec3, Option<&str>)> = references.iter().map(|r| (r.edge.0, None)).collect();
let frame = solidcraft_doc::hole_reference::reference_frame(&st, at, dir, &edges, true).map_err(|e| bad(cmd, e.to_string()))?;
let centre = frame.place(at, &distances).map_err(|e| bad(cmd, e.to_string()))?;
if !solidcraft_doc::hole_reference::on_reference_face(&st, &frame, centre) {
let what = if cmd == "solid.cylinder" { "cylinder's base" } else { "hole's centre" };
return Err(bad(cmd, format!("the reference distances put the {what} off its face")));
}
Ok(centre)
}
/// A hole's reference edges (#37): `references: [{edge: [x, y, z], distance: expr}]`, at most
/// two, each distance a length.
fn hole_reference_params(s: &Session, p: &Value, cmd: &str) -> Result<Vec<solidcraft_doc::HoleReference>> {
+84 -4
View File
@@ -470,9 +470,11 @@ impl Session {
}
/// Take up a save that ran on `done`, a [`Session::scratch`] copy of this session that
/// `file.save` or `file.save_as` ran on (on another thread, so the window keeps drawing)
/// while this one did not change: the name the save gave the design, its
/// file, and its saved state, as running the command here would have left them.
/// `file.save` or `file.save_as` ran on (on another thread, so the window keeps drawing):
/// the name the save gave the design, its file, and its saved state, as running the command
/// here would have left them. What is saved is the design the file holds, `done`'s, not this
/// one's: a change made here while the save was written is not in the file, so it stays
/// unsaved (#6003).
pub fn adopt_save(&mut self, done: &Session) {
if done.doc.name != self.doc.name {
let (from, to) = (self.doc.name.clone(), done.doc.name.clone());
@@ -481,7 +483,8 @@ impl Session {
self.path.clone_from(&done.path);
self.disk = done.disk;
self.read_only = done.read_only;
self.mark_saved();
self.saved = done.saved.clone();
self.saved_visibility = done.saved_visibility.clone();
}
/// Another program or computer wrote the design's file since it was opened or last saved:
@@ -923,6 +926,33 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// #6003: a session changed while its save ran on a copy takes up the save's file and name,
/// but only what the file holds counts as saved: the change stays unsaved, and undoing it
/// leaves nothing unsaved again. A hidden body is a change too.
#[test]
fn a_change_made_while_a_copy_saves_stays_unsaved() {
let dir = std::env::temp_dir().join(format!("solidcraft-adopt-changed-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("Bracket.solidcraft");
for hide in [false, true] {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10, "body_name": "Block"})).unwrap();
let mut copy = s.scratch();
copy.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
if hide {
s.execute("browser.visibility", &json!({"items": ["Block"], "visible": false})).unwrap();
} else {
s.execute("solid.sphere", &json!({"diameter": 4})).unwrap();
}
s.adopt_save(&copy);
assert_eq!((s.path.as_deref(), s.doc.name.as_str()), (path.to_str(), "Bracket"));
assert!(s.is_dirty(), "hide {hide}: the change is not in the file");
s.execute("edit.undo", &json!({})).unwrap();
assert!(!s.is_dirty(), "hide {hide}: back to what the file holds");
}
let _ = std::fs::remove_dir_all(&dir);
}
/// #5467: a script, the CLI or the control channel never gets a prompt. A Save over a file
/// another program or computer changed since it was opened is refused with an error naming
/// `overwrite`, which replaces it; a design opened read-only only saves to another file.
@@ -1146,6 +1176,56 @@ mod tests {
assert!(d.rejected.is_empty());
}
/// A design whose saved model would give a body back with other topology opens with the
/// bodies it was saved with. A pattern copy cut by an automatic extrude came back from the
/// native codec with two loose vertices more (10 faces, 18 edges, 12 vertices instead of 10);
/// such a body is not kept, the design is saved without its model and evaluated on opening.
#[test]
fn a_design_opens_with_the_bodies_it_was_saved_with_when_one_does_not_decode_to_itself() {
let mut session = Session::new(Document::new("Roundtrip"));
for (cmd, p) in [
("solid.cylinder", json!({"axis": "Z", "base": [-2, -17, -2], "body_name": "B2", "height": 11, "radius": 6})),
("sketch.create", json!({"plane": "YZ"})),
(
"sketch.line",
json!({"closed": true, "ids": ["w3a", "w3b", "w3c", "w3d"], "infer": true, "points": [[-3, -22], [1, -22], [1, -17], [-3, -17]]}),
),
("sketch.dimension", json!({"entities": ["w3a"], "type": "length", "value": 9})),
("solid.extrude", json!({"distance": -1, "operation": "cut"})),
("solid.box", json!({"corner": [27, -22, 0], "height": 12, "length": "d3", "operation": "new", "width": 3})),
("sketch.create", json!({"plane": "YZ"})),
(
"sketch.line",
json!({"closed": true, "ids": ["w6a", "w6b", "w6c", "w6d"], "infer": true, "points": [[-16, -23], [2, -23], [2, -15], [-16, -15]]}),
),
("sketch.dimension", json!({"entities": ["w6a"], "type": "length", "value": 11})),
("solid.extrude", json!({"distance": 18, "operation": "new"})),
("solid.extrude", json!({"distance": -5, "operation": "new"})),
("sketch.create", json!({"plane": "YZ"})),
("sketch.circle.center", json!({"center": [15, -8], "radius": 9})),
("solid.extrude", json!({"distance": "d8", "operation": "new"})),
("solid.pattern.circular", json!({"axis": "Y", "bodies": ["Body4"], "count": 3})),
("solid.extrude", json!({"distance": -16, "operation": "auto"})),
] {
session.execute(cmd, &p).unwrap();
}
let counts = |s: &Session| {
s.model
.state()
.bodies
.iter()
.map(|b| (b.name.clone(), b.body.face_count(), b.body.edge_count(), b.body.vertex_count()))
.collect::<Vec<_>>()
};
let bytes = session.design_container(solidcraft_io::DesignCompression::Fast, false, true).unwrap();
let path = std::env::temp_dir().join(format!("solidcraft-roundtrip-{}.solidcraft", std::process::id()));
std::fs::write(&path, &bytes).unwrap();
let mut reopened = Session::default();
reopened.execute("doc.open", &json!({"path": path})).unwrap();
std::fs::remove_file(&path).unwrap();
assert_eq!(counts(&reopened), counts(&session));
}
#[test]
fn saved_model_needs_no_meshes_and_is_adopted_only_for_its_document() {
let mut session = Session::new(Document::new("Saved model"));
+1 -1
View File
@@ -856,7 +856,7 @@ fn drive(s: &mut Session, p: &Value) -> Result<Value> {
let id = joint_id(s, p.get("joint"), cmd)?;
let kind = s.doc.assembly.joints.iter().find(|j| j.id == id).map(|j| j.kind).ok_or_else(|| bad(cmd, "joint"))?;
if kind == JointKind::Rigid {
return Err(bad(cmd, "a rigid joint does not move"));
return Err(bad(cmd, "a rigid joint does not move: joint.edit can make it revolute (turns), slider (slides) or cylindrical (both)"));
}
let mut q = values_param(s, p, kind, cmd)?;
if p.get("values").is_none() && p.get("value").is_none() {
+35
View File
@@ -192,6 +192,9 @@ fn rigid_cycles_solve_and_conflicts_report() {
let c = r["conflicts"].as_array().cloned().unwrap_or_default();
assert_eq!(c.len(), 1, "{r}");
assert!(c[0]["why"].as_str().unwrap_or("").contains("off by"), "{r}");
// A rigid joint has nothing to drive: the refusal names the types that move (#6981).
let e = s.execute("joint.drive", &json!({"joint": "Wrong", "value": 1})).map(|_| ()).unwrap_err().to_string();
assert!(e.contains("does not move") && e.contains("joint.edit") && e.contains("revolute") && e.contains("cylindrical"), "{e}");
// Suppressing it clears the conflict.
let r = run(&mut s, "joint.edit", json!({"joint": "Wrong", "suppressed": true}));
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0));
@@ -301,6 +304,38 @@ fn faces_mate_by_default() {
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 10.0)), Vec3::new(5.0, 5.0, 0.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 10.0)));
}
/// A move of a jointed part that the joint takes back: the model is evaluated for the
/// placements the solve leaves, so a mirror of the part's body, made from the root, is where a
/// fresh evaluation of the design puts it. The model was the one evaluated for the move before
/// the solve took it back, and its mirror didn't match the part.
#[test]
fn a_feature_reading_a_jointed_parts_placement_follows_the_solve() {
let mut s = Session::default();
for (cmd, p) in [
("component.create", json!({"grounded": true, "name": "Base"})),
("solid.box", json!({"corner": [-11, -25, 0], "height": 9, "length": 11, "width": 14})),
("component.create", json!({"grounded": false, "name": "Slide"})),
("solid.cylinder", json!({"base": [-2, -10, 7], "height": 7, "radius": 2})),
("component.activate", json!({"component": "root"})),
("joint.as_built", json!({"a": "Base:1", "b": "Slide:1", "name": "Rail", "type": "slider"})),
("solid.mirror", json!({"bodies": ["Body2"], "plane": "XY"})),
("occurrence.move", json!({"occurrence": "Slide:1", "translate": [-10, -18, 2]})),
] {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"));
}
let boxes = |st: &solidcraft_doc::ModelState| {
st.bodies
.iter()
.map(|b| (b.name.clone(), b.body.outline_bounds()))
.map(|(n, bb)| format!("{n} {:.6?} {:.6?}", bb.min, bb.max))
.collect::<Vec<_>>()
};
let mut fresh = solidcraft_doc::Model::new();
fresh.no_cache = true;
fresh.evaluate(&s.doc);
assert_eq!(boxes(&s.model.state()), boxes(&fresh.state()));
}
/// #1239: two parts as bodies of one component (a multi-body STEP import). The refusal names the
/// way out, and after Create Components from Bodies the same origins make the joint.
#[test]
+3 -1
View File
@@ -37,7 +37,7 @@ mod features;
mod features_more;
pub(crate) use face::{FaceRefForm, face_ref_form};
pub(crate) use features::profiles as feature_profiles;
pub(crate) use features::{EdgeRefForm, edge_ref_form, sketch_key};
pub(crate) use features::{EdgeRefForm, axis_dir_form, edge_ref_form, sketch_key};
pub use features::{auto_operation, extrude_reach};
pub use sketch_import::drawing_import_command;
pub use sketch_modify::{extend_arc_preview, extend_preview, trim_piece};
@@ -71,6 +71,7 @@ mod parameter_sweep;
mod parameters;
mod plastic;
mod render;
mod render_ui;
#[cfg(test)]
mod script_tests;
mod sheet;
@@ -320,6 +321,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
v.extend(view_dxf::COMMANDS.iter());
v.extend(diagnostics::COMMANDS.iter());
v.extend(render::COMMANDS.iter());
v.extend(render_ui::COMMANDS.iter());
v
}
+64
View File
@@ -1383,3 +1383,67 @@ fn a_body_seen_in_another_placement_copies_from_there() {
let state = s.model.state();
assert_eq!(s.doc.model_body_component(state.body("Cube (2)").unwrap()), 0);
}
/// A nested assembly's body listing, and the indexed placement agreeing with the plain walk
/// (#7502: listing a subtree must cost the subtree, not the subtree times the design).
#[test]
fn occurrence_bodies_lists_a_nested_assembly() {
let mut s = Session::default();
let (_outer, outer) = created(&mut s, "Outer");
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Shell"}));
let (_middle, middle) = created(&mut s, "Middle");
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "body_name": "Plate"}));
let (_inner, inner) = created(&mut s, "Inner");
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "body_name": "Pin"}));
let deep = [outer, middle, inner];
let sorted = |v: Vec<String>| {
let mut v = v;
v.sort();
v
};
assert_eq!(sorted(s.occurrence_bodies(&deep, true).unwrap()), ["Pin"]);
assert_eq!(sorted(s.occurrence_bodies(&[outer, middle], true).unwrap()), ["Pin", "Plate"]);
assert_eq!(sorted(s.occurrence_bodies(&[outer], true).unwrap()), ["Pin", "Plate", "Shell"]);
assert_eq!(sorted(s.occurrence_bodies(&[outer, middle], false).unwrap()), ["Plate"]);
// The indexed placement answers exactly what the plain walk does.
for path in [&deep[..], &[outer, middle][..], &[outer][..]] {
let (indexed, walked) = (s.occurrence_placement(path).unwrap(), s.occurrence_placement_walk(path).unwrap());
assert_eq!(indexed.component, walked.component);
assert_eq!(indexed.name, walked.name);
assert_eq!(indexed.world, walked.world);
assert_eq!(indexed.bodies.len(), walked.bodies.len());
for (a, b) in indexed.bodies.iter().zip(&walked.bodies) {
assert_eq!((a.source.as_str(), a.world.as_str()), (b.source.as_str(), b.world.as_str()));
}
}
}
/// A burst of other paths retires single remembered answers instead of clearing the whole
/// cache, so the active occurrence's listing survives and needn't re-expand (#7502).
#[test]
fn occurrence_bodies_cache_retires_one_answer_when_full() {
let mut s = Session::default();
let (_assembly, assembly) = created(&mut s, "Assembly");
let (part, _) = created(&mut s, "Part");
run(&mut s, "solid.box", json!({"length": 1, "width": 1, "height": 1, "body_name": "Part"}));
let mut copies = Vec::new();
for i in 0..130 {
let o = run(&mut s, "occurrence.copy", json!({"component": part, "translate": [i as f64 * 10.0, 0.0, 0.0]}))["occurrence"].as_u64().unwrap();
copies.push(o);
}
// The active listing (the whole assembly) is asked first and remembered.
let root = vec![assembly];
assert_eq!(s.occurrence_bodies(&root, true).unwrap().len(), 131);
// Then a burst of per-copy queries: 260 more keys, over the cache's 256-entry limit.
for o in &copies {
let path = vec![assembly, *o];
assert_eq!(s.occurrence_bodies(&path, false).unwrap().len(), 1);
assert_eq!(s.occurrence_bodies(&path, true).unwrap().len(), 1);
}
let guard = s.occurrence_bodies_cache.lock().unwrap();
let cache = guard.as_ref().unwrap();
assert!(cache.bodies.contains_key(&(root.clone(), true)), "the active listing survives a burst of other paths");
assert!(cache.bodies.len() <= 256, "the cache stays bounded at its limit");
drop(guard);
assert_eq!(s.occurrence_bodies(&root, true).unwrap().len(), 131);
}
+11 -2
View File
@@ -1044,9 +1044,13 @@ fn dielectric_preflight_and_start_resolve_refraction_without_broadening_boundari
let mut s = Session::default();
s.set_render_job_host(Some(host.clone())).unwrap();
s.execute("solid.box", &json!({"length":20,"width":20,"height":2,"body_name":"Glass"})).unwrap();
// The default view sends the single pixel's ray exactly through a box edge, which the optics reject
// as an ambiguous interface normal. Whether the job then ends Failed or Cancelled would depend on
// whether the cancel below beats the render thread, so view the glass off-axis.
let camera = solidcraft_render::Camera { yaw: 0.3, pitch: 0.4, ..Default::default() };
let document = s.doc.clone();
let before = (s.revision, s.evaluations, s.is_dirty(), s.undo.len(), s.redo.len());
let params = json!({"integrator":"raytrace","settings":{"width":1,"height":1},"raytrace":{"dielectrics":[{
let params = json!({"integrator":"raytrace","settings":{"width":1,"height":1,"camera":camera},"raytrace":{"dielectrics":[{
"body_index":0,"body_name":"Glass","boundary":"closed_box","ior":1.5,"absorption_per_mm":[0,0,0],"thickness_mm":0
}]}});
for command in ["render.preflight", "render.start"] {
@@ -1065,7 +1069,12 @@ fn dielectric_preflight_and_start_resolve_refraction_without_broadening_boundari
let handle = s.render_job_handle(id).unwrap();
s.execute("render.cancel", &json!({"job":id})).unwrap();
let status = handle.wait_timeout(std::time::Duration::from_secs(10)).unwrap();
assert!(matches!(status.progress.phase, solidcraft_render::job::Phase::Cancelled | solidcraft_render::job::Phase::Completed));
assert!(
matches!(status.progress.phase, solidcraft_render::job::Phase::Cancelled | solidcraft_render::job::Phase::Completed),
"phase {:?}, error {:?}",
status.progress.phase,
status.error
);
}
}
for boundary in ["closed_mesh", "thin_sheet"] {
+46
View File
@@ -0,0 +1,46 @@
//! Planned Render interactions, with explicit non-mutating refusals.
use super::CommandSpec;
macro_rules! waiting {
($id:literal, $label:literal, $tab:literal, $panel:literal, $icon:literal, $reason:literal) => {
CommandSpec::new($id, $label, |_, _| Err(crate::EngineError::Other(concat!("not supported yet: ", $reason).into())))
.enabled(|_| Err(concat!("not supported yet: ", $reason).into()))
.planned("5493")
.noundo()
.doc_only()
.at($tab, $panel)
.icon($icon)
.params("No supported parameters. Always refuses without changing the design or history.")
};
}
pub static COMMANDS: &[CommandSpec] = &[
waiting!("appearance.paint", "Paint Appearance", "RENDER APPEARANCE", "APPEARANCE", "appearance", "product picking and appearance painting"),
waiting!("appearance.pick_up", "Pick Up Appearance", "RENDER APPEARANCE", "APPEARANCE", "select", "product picking and appearance sampling"),
waiting!("appearance.replace", "Replace Appearance", "RENDER APPEARANCE", "APPEARANCE", "appearance", "replacing all uses of an appearance"),
waiting!("render.lights.solo", "Solo Light", "RENDER SCENE", "LIGHTS", "render_scene", "light visibility and solo controls"),
waiting!("render.compare", "Compare Images", "RENDER", "RESULT", "compare", "retained image comparison"),
waiting!("render.batch", "Render All Shots", "RENDER", "BATCH", "render_image", "the saved-shot render queue"),
];
#[cfg(test)]
#[test]
fn render_workspace_planned_commands_refuse_without_mutation() {
use serde_json::{Value, json};
let mut session = crate::Session::default();
session.execute("render.settings.set", &json!({"name":"Studio","set":{"width":800}})).unwrap();
session.execute("render.settings.set", &json!({"name":"Studio","set":{"width":900}})).unwrap();
session.execute("edit.undo", &json!({})).unwrap();
let document = session.doc.clone();
let before = (session.undo.len(), session.redo.len(), session.revision, session.evaluations, session.is_dirty());
for command in COMMANDS {
assert!(command.planned.is_some());
assert!(!command.info(&session).enabled);
for params in [json!({}), Value::Null, json!([]), json!({"target":1e300,"name":"hostile"})] {
let error = session.execute(command.id, &params).unwrap_err().to_string();
assert!(error.contains("not supported yet"), "{error}");
assert!(std::sync::Arc::ptr_eq(&document, &session.doc));
assert_eq!(before, (session.undo.len(), session.redo.len(), session.revision, session.evaluations, session.is_dirty()));
}
}
}
@@ -360,3 +360,57 @@ fn construction_axis_revolve_rejects_invalid_axes_atomically() {
assert!(s.model.state().bodies.is_empty());
}
}
#[test]
fn world_axis_from_timeline_inspect_round_trips() {
for (axis, radial, axial) in [("X", Vec3::Y, Vec3::X), ("Y", Vec3::X, Vec3::Y), ("Z", Vec3::X, Vec3::Z)] {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"name":"Profile","plane":{"origin":[0,0,0],"x_dir":radial,"y_dir":axial}}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[2,0],"p1":[4,10]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.revolve", json!({"sketch":"Profile","axis":axis,"angle":"90 deg","operation":"new","name":"Turn"}));
let inspected = run(&mut s, "timeline.inspect", json!({"feature":"Turn"}));
let stored_axis = inspected["definition"]["axis"].clone();
assert_eq!(stored_axis, json!({"type":"world","axis":axis}));
let original = s.doc.find_feature("Turn").unwrap().kind.clone();
let original_measure = solidcraft_kernel::measure(&s.model.state().bodies[0].body).unwrap();
run(&mut s, "edit.undo", json!({}));
assert!(s.model.state().bodies.is_empty());
run(&mut s, "solid.revolve", json!({"sketch":"Profile","axis":stored_axis,"angle":"90 deg","operation":"new","name":"Turn"}));
assert_eq!(s.doc.find_feature("Turn").unwrap().kind, original);
let state = s.model.state();
assert_eq!(state.bodies.len(), 1);
let measure = solidcraft_kernel::measure(&state.bodies[0].body).unwrap();
// A quarter of an annular cylinder: pi * (4^2 - 2^2) * 10 / 4.
assert!((measure.volume - 30.0 * PI).abs() < 0.03, "{axis}: {}", measure.volume);
assert!((measure.volume - original_measure.volume).abs() < 1e-6);
assert!(measure.centroid.dist(original_measure.centroid) < 1e-6);
}
}
#[test]
fn invalid_world_axis_objects_are_rejected_atomically() {
let mut s = Session::default();
profile(&mut s, false);
let before = s.doc.to_json();
for axis in [
json!({"type":"world"}),
json!({"type":"world","axis":null}),
json!({"type":"world","axis":1}),
json!({"type":"world","axis":[0,1,0]}),
json!({"type":"world","axis":{}}),
json!({"type":"world","axis":""}),
json!({"type":"world","axis":"x"}),
json!({"type":"world","axis":"y"}),
json!({"type":"world","axis":"z"}),
json!({"type":"world","axis":"W","dir":[0,1,0]}),
] {
let error = s.execute("solid.revolve", &json!({"sketch":"Profile","axis":axis,"angle":"90 deg","operation":"new"})).unwrap_err();
let message = error.to_string();
assert!(matches!(error, crate::EngineError::BadParams { .. }), "{axis}: {message}");
assert!(message.contains("world axis") && message.contains("X, Y or Z"), "{axis}: {message}");
assert_eq!(s.doc.to_json(), before);
assert!(s.model.state().bodies.is_empty());
}
}
+15 -3
View File
@@ -51,7 +51,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.params(concat!("points: [[x,y] | \"l1.end\" | {at, ref}…] (x and y each a number or an expression over the design's parameters, \"W / 2\": such a point is fixed where it comes to and follows the parameters; likewise wherever a point argument also takes a point id), closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.draw.dimensioned", "Draw Dimensioned Sketch Geometry", draw_dimensioned).icon("dimension")
.enabled(in_sketch)
.params(concat!("command: sketch creation command, params: object, dimensions: object of role to numeric or expression value; creates geometry and all driving dimensions atomically in one undo step", created_doc!())),
.params(concat!("command: sketch creation command, params: object, dimensions: object of role to numeric or expression value, or `name = expression` (also `#name = expression`), which defines the user parameter `name` as sketch.dimension does (the reply's `defined` lists the names); creates geometry and all driving dimensions atomically in one undo step", created_doc!())),
CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point)
.at("SKETCH", "CREATE")
.icon("rect")
@@ -1419,8 +1419,20 @@ fn polygon(s: &mut Session, p: &Value, cmd: &str, center: Vec2, r_vertex: f64, s
cons.push(add_c(sk, ConstraintKind::PointOnCurve { p: *pt, c: circ })?);
}
} else {
for li in &l {
cons.push(add_c(sk, ConstraintKind::Tangent { a: *li, b: circ })?);
// Equal sides tangent to one circle don't make a polygon regular: every other corner
// stays free (a rhombus round a circle), and a drag pulled it out of shape (#7395).
// The corners on a second circle, concentric, make it regular as the inscribed one
// is; one side tangent to the support circle then sets its size (the others follow).
let corner = sk.add_circle(center, r_vertex, None, None)?;
if let Some(cu) = sk.curves.get_mut(corner) {
cu.construction = true;
}
cons.push(add_c(sk, ConstraintKind::Concentric { a: circ, b: corner })?);
for pt in &pts {
cons.push(add_c(sk, ConstraintKind::PointOnCurve { p: *pt, c: corner })?);
}
if let Some(first) = l.first() {
cons.push(add_c(sk, ConstraintKind::Tangent { a: *first, b: circ })?);
}
}
Ok((ids_of(sk, &l), cons))
@@ -8,8 +8,12 @@ use solidcraft_sketch::{ConstraintKind, CurveKind, Sketch};
use super::sketch::{add_c, constraint_result, dof_now, edit, listed, redundancy, residual_of};
use super::{CommandSpec, in_sketch};
use crate::params::{bad, num, req_vec2, str_, string_list, vec2};
use crate::schema::sketch_constraints::{ToggleDiameterParams, ToggleDiameterResult};
use crate::schema::{TypedContract, output_schema_for};
use crate::{EngineError, Result, Session};
const TOGGLE_DIAMETER_CONTRACT: TypedContract<ToggleDiameterParams> = TypedContract::new();
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.constraint.curvature", "Curvature", c_smooth)
.at("SKETCH", "CONSTRAINTS")
@@ -44,6 +48,12 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sketch.toggle_driven", "Toggle Driven Dimension", toggle_driven).icon("dimension")
.enabled(in_sketch)
.params("constraint: dimension id, driven?: bool (default toggles); a driving dimension gets a parameter with its current value"),
CommandSpec::new("sketch.toggle_diameter", "Toggle Radius / Diameter", crate::checked!("sketch.toggle_diameter", toggle_diameter))
.icon("dimension")
.enabled(in_sketch)
.contract(&TOGGLE_DIAMETER_CONTRACT)
.output(output_schema_for::<ToggleDiameterResult>)
.params("constraint: a circle's or an arc's radius or diameter dimension id; diameter?: bool (default: the other one); sketch?: id|name (default: the one being edited). The circle stays the same size: its parameter is halved or doubled (a plain value is rewritten, an expression becomes `(e) / 2` or `2 * (e)`); refused when anything else uses that parameter"),
CommandSpec::new("sketch.glyphs", "Constraint Glyphs", glyphs).icon("eye")
.enabled(in_sketch)
.noundo()
@@ -408,6 +418,60 @@ fn toggle_driven(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"constraint": cid, "driven": driven, "sketch": info}))
}
fn toggle_diameter(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.toggle_diameter";
let cid = str_(p, "constraint").ok_or_else(|| bad(cmd, "`constraint` must be a dimension id"))?.to_string();
let want = crate::params::bool_(p, "diameter");
let sketch = super::sketch::target_sketch(s, p, cmd)?;
let (diameter, info) = edit(s, p, cmd, true, |sk, doc| {
let c = sk.constraints.iter().find(|c| c.id == cid).cloned().ok_or_else(|| bad(cmd, format!("no constraint `{cid}`")))?;
let round = |curve: usize| matches!(sk.curves.get(curve).map(|c| &c.kind), Some(CurveKind::Circle { .. } | CurveKind::Arc { .. }));
let (curve, value, now) = match c.kind {
ConstraintKind::Radius { c, value } if round(c) => (c, value, false),
ConstraintKind::Diameter { c, value } if round(c) => (c, value, true),
_ => return Err(bad(cmd, format!("`{cid}` is not a circle's or an arc's radius or diameter dimension"))),
};
let diameter = want.unwrap_or(!now);
if diameter == now {
return Ok(diameter);
}
let (kind, value) = if diameter {
(ConstraintKind::Diameter { c: curve, value: 2.0 * value }, 2.0 * value)
} else {
(ConstraintKind::Radius { c: curve, value: value / 2.0 }, value / 2.0)
};
// Its parameter now holds the other measure, so the circle keeps its size; another user
// of that parameter would change size with it.
if let Some(name) = &c.param {
let others: Vec<String> = doc
.param_users(name)
.into_iter()
.filter(
|u| !matches!(u, solidcraft_doc::ParamUser::Dimension { feature, constraint, .. } if *feature == sketch && *constraint == cid),
)
.map(|u| u.describe())
.collect();
if !others.is_empty() {
return Err(bad(cmd, format!("`{name}` is also used by {}; switching `{cid}` would change them", others.join(", "))));
}
let e = doc.param(name).map(|p| p.expr.clone()).ok_or_else(|| bad(cmd, format!("no parameter `{name}`")))?;
let e = if solidcraft_doc::expr::references(&e).is_empty() {
doc.design_length(value).1
} else if diameter {
format!("2 * ({e})")
} else {
format!("({e}) / 2")
};
doc.set_param(name, &e, None, None).map_err(|e| bad(cmd, e.to_string()))?;
}
let slot = sk.constraints.iter_mut().find(|c| c.id == cid).ok_or_else(|| bad(cmd, "constraint"))?;
slot.kind = kind;
Ok(diameter)
})?;
let reply = ToggleDiameterResult { constraint: cid, diameter, sketch: info, param_warnings: Vec::new() };
serde_json::to_value(reply).map_err(|e| EngineError::Other(e.to_string()))
}
/// Where a constraint's glyph goes (sketch coordinates): beside the middle of its first curve,
/// or at its point.
fn anchor(sk: &Sketch, k: &ConstraintKind) -> Option<Vec2> {
@@ -285,6 +285,62 @@ fn smart_constrain_and_driven_toggle() {
assert_eq!(r["driven"], false);
}
/// #7499: a circle's or an arc's dimension switches between its radius and its diameter, the
/// circle keeping its size: the parameter is halved or doubled, an expression wrapped.
#[test]
fn a_radius_dimension_switches_to_a_diameter_and_back() {
use solidcraft_sketch::ConstraintKind;
let mut s = new_sketch();
let dim = |s: &Session, param: &str| sketch(s).constraints.iter().find(|c| c.param.as_deref() == Some(param)).unwrap().clone();
let c1 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone();
let d = run(&mut s, "sketch.dimension", json!({"entities": [c1]}));
let p1 = d["param"].as_str().unwrap().to_string();
let did = dim(&s, &p1).id;
assert!(matches!(dim(&s, &p1).kind, ConstraintKind::Diameter { .. }), "a circle is dimensioned by its diameter");
let r = run(&mut s, "sketch.toggle_diameter", json!({"constraint": did}));
assert_eq!(r["diameter"], false, "{r}");
assert!(matches!(dim(&s, &p1).kind, ConstraintKind::Radius { value, .. } if (value - 10.0).abs() < 1e-9), "{:?}", dim(&s, &p1).kind);
assert_eq!(s.doc.param(&p1).unwrap().expr, "10 mm");
assert!((sketch(&s).radius(0).unwrap() - 10.0).abs() < 1e-9, "the circle keeps its size");
// Asking for what it already is changes nothing; then back to the diameter.
let r = run(&mut s, "sketch.toggle_diameter", json!({"constraint": did, "diameter": false}));
assert_eq!(r["diameter"], false, "{r}");
run(&mut s, "sketch.toggle_diameter", json!({"constraint": did}));
assert!(matches!(dim(&s, &p1).kind, ConstraintKind::Diameter { value, .. } if (value - 20.0).abs() < 1e-9));
assert_eq!(s.doc.param(&p1).unwrap().expr, "20 mm");
// An expression is wrapped, so it still reads the parameters it names.
run(&mut s, "parameters.add", json!({"name": "w", "expression": "30 mm"}));
let c2 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [50, 0], "radius": 10}))["curves"])[0].clone();
let p2 = run(&mut s, "sketch.dimension", json!({"entities": [c2], "value": "w"}))["param"].as_str().unwrap().to_string();
let d2 = dim(&s, &p2).id;
run(&mut s, "sketch.toggle_diameter", json!({"constraint": d2}));
assert_eq!(s.doc.param(&p2).unwrap().expr, "(w) / 2");
assert!((sketch(&s).radius(1).unwrap() - 15.0).abs() < 1e-6, "{:?}", sketch(&s).radius(1));
// Another user of the parameter would change size with it: refused, nothing changes.
run(&mut s, "parameters.add", json!({"name": "k", "expression": format!("{p1} + 1 mm")}));
let e = s.execute("sketch.toggle_diameter", &json!({"constraint": did})).unwrap_err().to_string();
assert!(e.contains("also used by parameter k"), "{e}");
assert!(matches!(dim(&s, &p1).kind, ConstraintKind::Diameter { .. }));
assert_eq!(s.doc.param(&p1).unwrap().expr, "20 mm");
// An arc's driven radius becomes its driven diameter; a line's length is no radius.
let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [0, 80], "start": [20, 80], "sweep": 90}))["curves"])[0].clone();
run(&mut s, "sketch.dimension", json!({"entities": [a], "driven": true}));
let arc = sketch(&s).curves.iter().position(|c| c.id == a).unwrap();
let driven = sketch(&s).constraints.iter().find(|c| matches!(c.kind, ConstraintKind::Radius { c, .. } if c == arc)).unwrap().clone();
assert!(driven.driven && driven.param.is_none());
run(&mut s, "sketch.toggle_diameter", json!({"constraint": driven.id}));
let now = sketch(&s).constraints.iter().find(|c| c.id == driven.id).unwrap().clone();
assert!(now.driven && matches!(now.kind, ConstraintKind::Diameter { value, .. } if (value - 40.0).abs() < 1e-6), "{now:?}");
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, -50], [10, -50]]}))["curves"])[0].clone();
let pl = run(&mut s, "sketch.dimension", json!({"entities": [l], "value": 10}))["param"].as_str().unwrap().to_string();
let dl = dim(&s, &pl).id;
let e = s.execute("sketch.toggle_diameter", &json!({"constraint": dl})).unwrap_err().to_string();
assert!(e.contains("is not a circle's or an arc's radius or diameter dimension"), "{e}");
}
/// #4691: a point put on a line's midpoint snap is held there (a MidPoint constraint), and one
/// put on a curve stays on it, as a line's end does: they no longer move freely.
#[test]
+26
View File
@@ -79,6 +79,10 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("spline_cv")
.enabled(in_sketch)
.params(concat!("points: control points (2…500), degree 5", created_doc!())),
CommandSpec::new("sketch.spline.insert_point", "Add Spline Point", spline_insert_point)
.icon("spline")
.enabled(in_sketch)
.params("curve: fit point spline id, at: [x,y]: a fit point is inserted where the curve passes nearest to it, between its neighbours (the curve still runs through it and all its old points; constraints on them stay); end?: start|end: `at` becomes the spline's new first or last point instead, and the curve runs on to it (refused when that end is joined to other geometry). Control point splines take no new points yet. Returns point: the new point's id"),
CommandSpec::new("sketch.conic", "Conic Curve", conic)
.at("SKETCH", "CREATE")
.icon("conic")
@@ -678,6 +682,28 @@ fn spline_cv5(s: &mut Session, p: &Value) -> Result<Value> {
spline(s, p, "sketch.spline.control_point_5", true, 5)
}
fn spline_insert_point(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.spline.insert_point";
let at = req_vec2(cmd, p, "at")?;
let end = match p.get("end") {
None | Some(Value::Null) => None,
Some(Value::String(e)) if e == "start" => Some(true),
Some(Value::String(e)) if e == "end" => Some(false),
Some(_) => return Err(bad(cmd, "`end` must be start or end")),
};
let (point, info) = edit(s, p, cmd, false, |sk, _| {
let id = str_(p, "curve").ok_or_else(|| bad(cmd, "`curve` must be a spline id"))?;
let c = sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?;
let new = match end {
None => sk.insert_spline_point(c, at),
Some(at_start) => sk.extend_spline(c, at, at_start),
}
.map_err(|e| bad(cmd, e.to_string()))?;
sk.points.get(new).map(|q| q.id.clone()).ok_or_else(|| bad(cmd, "the new point is missing"))
})?;
Ok(json!({"point": point, "curve": p.get("curve"), "sketch": info}))
}
fn conic(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.conic";
let rho = num(p, "rho").unwrap_or(0.5);
+16 -2
View File
@@ -1,6 +1,6 @@
//! Shape creation and its typed driving dimensions form one command and one undo step.
use serde_json::{Value, json};
use solidcraft_doc::expr::Kind;
use solidcraft_doc::expr::{Kind, definition};
use crate::{
Result, Session,
@@ -38,7 +38,14 @@ pub(super) fn draw(s: &mut Session, p: &Value) -> Result<Value> {
.filter(|e| e.len() <= 4096)
.ok_or_else(|| bad(cmd, "dimension values must be bounded expressions or finite numbers"))?;
let kind = if role == "angle" || role == "sweep" { Kind::Angle } else { Kind::Length };
let evaluated = s.doc.eval(&e, kind)?;
// `name = expression` is checked by its right-hand side, read as the user parameter
// `sketch.dimension` defines will hold it (a bare number in the design's unit) (#7567).
let rhs = match definition(&e) {
Some((name, "")) => return Err(bad(cmd, format!("{role}: `{name} =` needs a value after the `=`"))),
Some((_, rhs)) => rhs,
None => e.as_str(),
};
let evaluated = s.doc.eval(&s.doc.with_design_unit(rhs, kind), kind)?;
if role == "length" {
overall_length = Some(evaluated);
}
@@ -85,6 +92,7 @@ pub(super) fn draw(s: &mut Session, p: &Value) -> Result<Value> {
curves.get(index).filter(|v| v.is_string()).cloned().ok_or_else(|| bad(cmd, "creator did not return the expected curve"))
};
let mut added = serde_json::Map::new();
let mut defined = Vec::new();
for (role, value) in dimensions {
let (entities, ty) = match (create, role.as_str()) {
("sketch.line" | "sketch.line.midpoint", "length") => (vec![curve(0)?], "length"),
@@ -120,12 +128,18 @@ pub(super) fn draw(s: &mut Session, p: &Value) -> Result<Value> {
dimension["sketch"] = sketch.clone();
}
let result = scratch.execute("sketch.dimension", &dimension)?;
if let Some(name) = result.get("defined").filter(|v| v.is_string()) {
defined.push(name.clone());
}
added.insert(role.clone(), result);
}
// Session::execute records the wrapper's before snapshot. Nested history must not leak.
scratch.undo.clear();
s.accept_computed(scratch);
out["dimensions"] = Value::Object(added);
if !defined.is_empty() {
out["defined"] = Value::Array(defined);
}
Ok(out)
}
@@ -227,3 +227,81 @@ fn dimensioned_authoring_uses_selected_occurrence_and_keeps_local_geometry() {
close(sk.points[a].pos.x, 0.0);
close(sk.points[b].pos.x, 5.0 / std::f64::consts::SQRT_2);
}
/// The expression of the parameter driving the first dimension `kind` matches.
fn driving(s: &Session, kind: fn(&ConstraintKind) -> bool) -> String {
let sk = sketch(s);
let c = sk.constraints.iter().find(|c| kind(&c.kind)).unwrap();
s.doc.param(c.param.as_deref().unwrap()).unwrap().expr.clone()
}
/// A typed box value `name = expression` defines the user parameter `name`, which drives the
/// dimension, as the Dimension tool's value does (#7567).
#[test]
fn a_typed_definition_defines_the_parameter_that_drives_the_dimension() {
let is_diameter = |k: &ConstraintKind| matches!(k, ConstraintKind::Diameter { .. });
let mut s = session();
let undo = s.undo.len();
let out = draw(&mut s, "sketch.circle.center", json!({"center":[0,0],"radius":3}), json!({"diameter":"width = 20"}));
assert_eq!(s.undo.len(), undo + 1);
assert_eq!(out["defined"], json!(["width"]), "{out}");
close(measure(&s, is_diameter), 20.0);
let width = s.doc.param("width").unwrap();
assert_eq!((width.expr.as_str(), width.unit.as_str(), width.model), ("20", "mm", false));
assert_eq!(driving(&s, is_diameter), "width");
run(&mut s, "edit.undo", json!({}));
assert!(sketch(&s).curves.is_empty() && s.doc.param("width").is_none());
// A bare number is in the design's unit, in the size checks too.
let mut s = session();
run(&mut s, "document.units", json!({"units":"in"}));
draw(&mut s, "sketch.circle.center", json!({"center":[0,0],"radius":3}), json!({"diameter":"#w = 2"}));
close(measure(&s, is_diameter), 50.8);
assert_eq!(s.doc.param("w").unwrap().unit, "in");
let mut s = session();
run(&mut s, "document.units", json!({"units":"in"}));
let slot = json!({"p0":[0,0],"p1":[60,0],"width":10,"connect":false});
draw(&mut s, "sketch.slot.overall", slot.clone(), json!({"length":"len = 2","width":"0.5"}));
close(measure(&s, |k| matches!(k, ConstraintKind::SlotOverallLength { .. })), 50.8);
// Both boxes of a rectangle may define a name.
let mut s = session();
let out = draw(&mut s, "sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[5,5]}), json!({"width":"w = 30","height":"h=10"}));
assert_eq!(out["defined"], json!(["h", "w"]), "{out}");
let is_length = |k: &ConstraintKind| matches!(k, ConstraintKind::Length { .. });
let sk = sketch(&s);
let mut lengths: Vec<_> =
sk.constraints.iter().filter(|c| is_length(&c.kind)).map(|c| solidcraft_sketch::measure_dimension(&sk, &c.kind).unwrap()).collect();
lengths.sort_by(f64::total_cmp);
assert_eq!(lengths.len(), 2);
close(lengths[0], 10.0);
close(lengths[1], 30.0);
}
#[test]
fn a_refused_typed_definition_changes_nothing() {
let mut s = session();
run(&mut s, "parameters.add", json!({"name":"taken","expression":"5","unit":"mm"}));
let circle = |value: &str| json!({"command":"sketch.circle.center","params":{"center":[0,0],"radius":3},"dimensions":{"diameter":value}});
let rect =
|w: &str, h: &str| json!({"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[5,5]},"dimensions":{"width":w,"height":h}});
for (params, says) in [
(circle("taken = 20"), "already a parameter"),
(circle("w ="), "needs a value"),
(circle("2 = 3"), ""),
(circle("w == 3"), ""),
(circle("w = -4"), "invalid diameter"),
(circle(&format!("w = {}", "1".repeat(4096))), "bounded"),
// The same name twice: the second box finds it taken.
(rect("w = 30", "w = 10"), "already a parameter"),
// A box's value can't use a name the other box defines: neither exists yet.
(rect("w = 30", "h = w / 2"), ""),
] {
let snapshot = serde_json::to_value(&*s.doc).unwrap();
let undo = s.undo.len();
let error = s.execute("sketch.draw.dimensioned", &params).expect_err(&params.to_string()).to_string();
assert!(error.contains(says), "{params}: {error}");
assert_eq!(serde_json::to_value(&*s.doc).unwrap(), snapshot, "{params}");
assert_eq!(s.undo.len(), undo);
}
}

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