Compare commits

...

62 Commits

Author SHA1 Message Date
bherbruck 7770f8982d Release 0.23.0 2026-10-25 18:18:39 -04:00
bherbruck 8e61eb7d6f Release 0.23.0: version bump, design format 37 (#6868)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:18:36 -04:00
James Barnette 2eaa15ea04 Context menu: a right-click on a face of a selected body keeps the selection (#5849) (#5915)
* claim: #5849

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

* Context menu: a right-click on a face of a selected body keeps the selection (#5849)

A right-click selects what is under the cursor first, so the menu is about it. The check
for "already selected" compared the pick with each selected item by identity, so a face
of a selected body never matched: right-clicking a face of a selected body replaced the
whole selection with that one face, and the face menu opened instead of the body menu.

A face, edge or vertex of a selected body now counts as part of the selection, whether the
body is selected on its own or through an occurrence. The right-click then leaves the
selection alone, as it already did for a face in a multi-selection of faces (#3733).

selection::same_item is unchanged: dialogs and Select Other compare picks with it, and
there a face of a selected body is a different pick.

Fixes #5849.

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

* Pass the held keys in the selected-body context menu test

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 20:41:52 +00:00
dudedagger42 f512d4a004 Move: placements of one part moved together each go the way asked (#4768) (#4849)
* Move: placements of one part moved together each go the way asked (#4768)

A part placed more than once (an imported assembly's corners, screws or
fan blades) is one body in its component. Moving several of its
placements together moved that body once per placement listed, each
placement turning the move its own way, so the copies flew apart.

When solid.move names one part through two or more placements, it now
moves those placements in the world, as occurrence.move does with space
world: a placement held by a rigid group (an imported assembly) moves with
the outermost placement holding it, one inside another being moved isn't
moved again, and root-component bodies listed are moved as bodies, all in
one undo step. Other moves are unchanged, and so is a move with copy.

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

* Free the grounded first part before the placement-move tests move it

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-25 20:28:06 +00:00
github-actions[bot] 62d55c3b29 Regenerate generated files and fold pending files [skip ci] 2026-10-25 21:31:35 +00:00
NOWN-DEV 826244bb28 Text parameters: a sketch text's words built from them follow every change (#727) (#6364)
* Test: a text's words follow its text parameters, and so does its cut

A sketch text whose words are an expression of text parameters (`Label + '-L'`): a parameter edit
and a configuration row that sets the parameter relabel the text, the text's cut follows, and the
expression is saved with the design.

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

* Text parameters: a sketch text's words built from them follow every change

A design's parameters are numbers; a text's words (a part label, a size code) could only be typed.
Text parameters hold words: `parameters.add` or `parameters.change` with `unit: "text"` (the
expression is the text, quotes optional), and `sketch.text` and `sketch.edit_text` take
`text_expression` (quoted texts and text parameters joined by `+`, `Label + '-L'`) instead of
`text`. The text is laid out anew from its expression on every evaluation, so a parameter edit,
a configuration row or a script relabels it and the features built on it (a cut, an emboss)
follow in the same rebuild. The design file keeps the text parameters and each text's expression
(one pending format change); a configuration table's parameter column can be a text parameter;
the Parameters dialog lists them under the unit `text`.

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

* Text parameters: the joints' same-but-features check compares the text parameters

The joints are solved once per change unless the design differs only in its features
(same_but_features, #6372), which names every Document field. The text parameters are a
field of their own and change what a text's words evaluate to, so two designs that differ
in them are not the same but for their features.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:24:23 +00:00
NOWN-DEV 111dbc6855 Render: the radiance bound check uses a range, as clippy 1.97 asks (#6650)
`cargo clippy --workspace --all-targets -- -D warnings` fails on Rust 1.97: the
reflection radiance bound in crates/render/src/raytrace/image.rs (#5494) trips
`clippy::manual_range_contains` (`v < 0.0 || v > 1e12`).

It now reads `!(0.0..=1e12).contains(&v)`. The two differ only for NaN, which the
`is_finite` check in front of it already refuses, so nothing else changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:23:04 +00:00
dudedagger42 ab7602fd0e Project: don't lay a second copy over geometry the sketch already shows (#4929)
* Project: don't lay a second copy over geometry the sketch already shows

A sketch on a face already holds the face's border, linked. Projecting that face
again, or one of the border's edges, added a second set of curves and points over
the first: on a rounded corner that shows as extra points and lines. Project now
skips a reference whose curves are all linked into the sketch already (lines,
circles, arcs), reports it as `already_projected`, and refuses a pick of nothing
new so it adds no undo step. Points on their own (the origin, a vertex) still
project.

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

* Projecting the face a sketch already shows adds no second copy in the face scenario

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-25 20:13:46 +00:00
HiroKenEz b1d4293cf0 One set of X, Y and Z colours for the origin axes, view triad and Move gizmo (#976) (#4430)
* claim: #976

* One set of X, Y and Z colours for every axis (#976)

The origin axes, the navigation cube's triad, 3D sketch triads and joint
origins drew `axis_x/y/z` (255,0,0 / 0,255,0 / 2,2,248), the view triad and
the headless picture drew `triad` (220,60,60 / 60,170,60 / 60,100,230) and
the Move gizmo drew `gizmo` (225,70,60 / 80,190,80 / 70,130,240). The
`triad` and `gizmo` tokens are gone: every axis reads `axis_x/y/z`, now the
view triad's values (the engine's `view::colors::AXIS_*`, so headless
pictures are unchanged, and the set the red-green table was measured for).
Older theme files' `triad` and `gizmo` lists still read and set the axes.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:59:36 +00:00
Homero Thompson 99d5b43024 Doc: the names of split faces no longer grow with every split (#5860) (#6045)
* claim: #5860

* Doc: stop the names of split faces from growing with every split

* Joints: the long-piece-name test takes the longest name a piece now gets

Split-face names are bounded at 512 bytes since #5860, so the test cannot
build one over 512; it takes the longest the history gives.

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:58:56 +00:00
raykholo f937905cf3 A sketch point on the origin can be dragged (#4719) (#4837) 2026-10-25 19:37:01 +00:00
raykholo 46f7d4d8dc Preferences: the web app's Updates section says how it updates (#5496) (#5500) 2026-10-25 19:29:04 +00:00
bherbruck 8071096a99 Docs: refresh every UI screenshot, GIF and video from the current UI (#6763)
* Docs: refresh every UI screenshot, GIF and video from the current UI

Re-record the README demo, the rover build, the sample plate and the class-A surface pictures
with 0.22.1 in the default dark theme. The demo script is fixed for the current sketch dimension
boxes and MCP file limits, and the rover replay and class-A pictures now have scripts.

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

* Docs: cut the idle time out of the demo and rover recordings

The README demo plays at 6x and the rover replay at 15x with a constant-rate camera orbit, with the stretches where nothing on screen changes cut out by the new trim_idle.py.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:21:01 +00:00
raykholo d17b70241f Command dialogs keep the value field focused after a viewport click (#4643) (#4652) 2026-10-25 19:13:51 +00:00
raykholo fbe778a1e8 Rigid Group counts picked instances and explains bodies in one component (#4790) (#4792) 2026-10-25 19:13:48 +00:00
momysnow 0252f0bf30 Render preview: test that it never paints the last Design frame (#5119) (#5120)
#5187 fixed the frozen Render preview: product frames now set ss_ready
themselves instead of keeping the flag from the last Design frame, so
paint() no longer blits that stale offscreen picture. Nothing tested it.

The new test draws a see-through Design frame (offscreen, for
order-independent transparency) and then an opaque product frame on the
same renderer, and checks the middle pixel shows the product. With the
product branch's ss_ready update removed it fails with the stale red
Design colour, [255, 129, 129, 255].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:12:50 +00:00
Igor Bronovskyi 633ce20563 i18n(uk): translate 188 pending messages (#4051)
* i18n(uk): translate 188 pending messages

Bring the Ukrainian catalog up to date with en.json after rebasing onto the latest main. Covers the crash-report form, AI preferences, preferences pages, solid tool help, free orbit and history controls. Keys stay sorted via cargo xtask lists.

* i18n(uk): translate 114 pending messages

Rebase onto the latest main and bring the Ukrainian catalog up to date with
en.json. The new messages cover the mouse-gesture editor and navigation
preset help, the frames and sheet-metal dialogs, fillet control radii, the
timeline folder actions, the motion keyframe controls, library appearance and
library file actions, and the measure dialog.

Also correct "Add Key" to "Додати ключовий кадр": the only user of that
message is the motion keyframe button, and the catalog already translates
"Keys" as keyframes.

Keys stay sorted; cargo xtask lists --check and the i18n catalog tests pass.

* Sort the Ukrainian catalogue

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:10:13 +00:00
dudedagger42 0c0c4db612 Home: a theme's File view follows its own colours where it sets none (#4868) (#4924)
* Home: a theme's File view follows its own colours where it sets none (#4868)

and no built-in theme sets them, so every theme kept Dark's or Light's blue rail and grey cards
under its own app bar and headings. A theme that leaves them unset now gets them from its own
palette: page and cards from its panel and dialog, the rail from its accent deepened as Dark's
and Light's are, the primary action from its soft accent, and each ink the one of its own colours
that reads best there. Dark and Light themselves are unchanged.

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

* Leave a theme that sets no colours as its base palette

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-25 19:05:48 +00:00
shop-3d 16bb97f065 I18n danish cad help 20261021 (#4223)
* i18n(da): complete upstream catalog and align CAD help terminology

Add 371 missing Danish translations, refresh changed source messages,
and independently rewrite six help descriptions from SolidCraft sources.
Preserve all 2397 keys, canonical ordering and placeholders.

* Sort the Danish catalogue

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:59:18 +00:00
Taha Zarif 3e6c039985 Fix/recovery metadata bounds (#5578)
* test: cover malformed and unbounded recovery metadata

Add regression tests for oversized JSON, mismatched entry IDs, and
non-regular recovery metadata. These expose failures in the baseline
recovery reader before the implementation fix.

Signed-off-by: Taha Zarif <tahazarif380@gmail.com>

* fix: bound recovery metadata reads and validate entry identity

Reject oversized and non-regular recovery metadata before reading it,
and bound the read if a file grows after inspection. Require each JSON
entry ID to match its filename for orphan listing and recovery loading,
preventing unrelated save paths from being attached to a recovered design.

Signed-off-by: Taha Zarif <tahazarif380@gmail.com>

* Format recovery.rs

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

---------

Signed-off-by: Taha Zarif <tahazarif380@gmail.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:49:55 +00:00
Carlos Keese 7734ba2300 Offset Face: split a periodic face off its seam first, as booleans already do (#6226) (#6227)
* claim: #6226

* Offset Face: split a face off its seam and try again, as booleans do (#6226)

A hole's wall arrives from a STEP file as one whole turn bounded by one wire that
runs along its seam twice, where SolidCraft builds the same wall as two halves.
Offset Face heals the body before it moves anything, and healing such a face
fails inside truck with `This wire is not simple`, so the panic guard turned the
whole operation into a kernel internal error. Since importing is how a part with
a hole usually arrives, Offset Face did not work on imported parts with holes at
all.

`seam_faces` already answers this for booleans: split every such face in two
along a plane through its axis, run the operation, and compose the history back
over the body as it was given. This gives the offset path the same retry.

- `seam_faces::one_body` is `boolean`'s answer for an operation that takes one
  body, with `needed_one` beside `needed`.
- `offset_faces` now does its healing, its face picking and its offset inside one
  closure, so the retry runs the whole of it on the split body; the first attempt
  is guarded, so the failure is an error the retry can act on rather than an
  unwind. The faces are picked per body, because the pieces are numbered
  differently once a face is split — the points do not move, so they pick the
  same surface either way.

The regression test is in `seam_faces_tests.rs`, beside the boolean ones: a
20 x 20 x 10 block with a 6 mm hole, read back from STEP, whose top moves 2 mm
and lands on the same volume as moving the built body's top, with its history
intact.

Checked against `main` before and after: the three kernel tests that fail here
(`blend::…seam_twice`, `step_in::…parallel_body_preparation`,
`tests::a_box_cut_round_a_small_hole…`) fail the same way without this change.

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

---------

Co-authored-by: CarlosKeese <dudukeese@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 18:49:44 +00:00
Vetle Marentius Nilsen 0e916ee598 Add Norwegian Bokmål interface translation (#4544) (#4556)
* claim: #4544

* Add Norwegian Bokmål translation

* Refresh Norwegian Bokmål translations against current main

* Drop Norwegian messages whose ids are no longer in the English catalogue

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:49:00 +00:00
raykholo 87c9a37663 Cut and Intersect say which component holds the body they missed (#4639) (#4641) 2026-10-25 18:42:50 +00:00
dudedagger42 10124c8ccf Redefine Sketch Plane names the plane a sketch is on now (#5103) (#5545)
* claim: #5103

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

* Redefine Sketch Plane names the plane a sketch is on now (#5103)

For a sketch with no body-face attachment the panel only said so. It now shows
a Current plane row: the origin plane, a construction plane's name, or the
plane it is offset from, turned from or flipped from. English, Japanese and
Chinese messages added.

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

* Drop needless to_string calls in the plane name format

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-25 18:42:24 +00:00
Kai Evans 0870d6474a Themes: Okibi (dark) and Akari (light), from the Okibi design system (#5730) (#5731)
* Themes: Okibi (dark) and Akari (light), from the Okibi design system

Two built-in themes mapped from the Okibi design system v1.8
(https://kaievns.github.io/okibi-akiri-design-system-spec/): cool charcoal
ash with a single ember accent, and its warm-paper light twin. Every one of
the 156 colours is set, so nothing falls back to the Dark or Light palette.

* Give the Okibi and Akari themes an icon palette

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:42:19 +00:00
JK3D e9b9c8f697 Joints: apply a joint solve through indexes instead of searches (#6392) (#6393)
* claim: #6392

* perf: apply a joint solve with indexed joints and occurrences

Every change to a design re-solves its joints and applies the solution. Applying it searched
the joints for each value and, for each occurrence the solve places, searched the occurrences
for it and walked its parents' first occurrences by searching again.

Joints and occurrences are now indexed once, keeping the first of each id as the searches
found it, and the parents' transform is read through the index as the loop leaves it
(component_transform shares the walk, transform_through).

Synthetic design, headless (2000 components at the root with one component inside each, 4000
joints, a solution placing every occurrence): apply 14.1 -> 2.2 ms (median of 12).

The new test applies a solution that moves a component and the components inside it, and
checks each lands where the solution put it, read through its parent as the loop has just
moved it.
2026-10-25 18:42:10 +00:00
JK3D cbcf1fe716 component.list: group bodies, features and occurrences once (#6394) (#6395)
* claim: #6394

* perf: group bodies, features and occurrences once in component.list

component.list searched every body, every feature (three times) and every occurrence (twice)
for each component, so its cost grew with components times contents. Bodies, features,
occurrences and children are now grouped by component once, keeping their order, and each
component reads its own groups. The output is unchanged.

The new test lists nested components and checks each gets only its own bodies, features,
sketches, occurrences and children, in timeline order.
2026-10-25 18:42:06 +00:00
raykholo e21760e46d Sketch Move with a rotation swaps or drops horizontal and vertical constraints (#4645) (#4651) 2026-10-25 18:35:15 +00:00
raykholo df01e59716 Sketch: an x distance beside a y distance on the same points isn't refused (#4352) (#4387) 2026-10-25 18:27:40 +00:00
Jesper We 3045d0787e Dialog curve picks keep their sketch (#4057) (#4081)
* Dialog curve picks keep their sketch (#3726)

* Format

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

* Format

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

* Adapt context-menu test to Sel::SketchCurve's sketch field

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

* Path Flange picks keep their sketch too

Path Flange (#4096) landed after this PR and still looked its path up in the newest sketch with those ids.

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:20:03 +00:00
raykholo d876f475a6 Sketch: a distance dimension of 0 keeps the sketch fully constrained (#4368) (#4397) 2026-10-25 18:19:55 +00:00
neosandeep24 ef1a400bb3 Home: recent card no longer spins forever when its picture can't be made (#4655) (#4721)
* Home: stop a recent card spinning forever when its picture can't be made (#4655)

A recent design whose thumbnail job returned no picture (empty or unreadable design) kept the loading placeholder, which draws a spinner, with no retry. Record such keys as a terminal 'failed' state and draw a box placeholder instead; opening or saving the design clears it and tries again. Sample previews are untouched: a failed sample build still keeps its shipped picture.

* Format the gallery sample row

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:19:39 +00:00
Joseph 53c84eb3a7 Toolbar: a drop-down row's tooltip names its rebound key (#6290)
The row itself showed the key the command runs from (#1810), but its help
tooltip still named the default one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:10:09 +00:00
Joseph 9a7a75788e Sketch keys: a variant with its own key leaves its family's key alone (#6289)
With G bound to the centre rectangle, G, Escape, R started the centre
rectangle again, because R's family remembered the variant used last
(#4308). A remembered variant that has a key of its own no longer takes
over the family's key; one without a key still comes back on it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:09:58 +00:00
ABDULLAH 92dc6bc987 Assistant: pick the provider first, then its model (#5564) (#5565)
When an agent's models come from several providers (Hermes and OpenCode
name each one "Provider · model" or say "Provider: ..." in its
description), the chat header shows a provider drop-down before the model
drop-down, and the model drop-down lists only that provider's models
without the provider in front. It opens on the provider of the model in
use; picking a provider alone changes nothing. ui.agent_chat takes
provider and reports providers and provider.
2026-10-25 18:09:32 +00:00
Luis Amaral b5af4a80fa i18n(pt-BR): translate 319 missing messages (#5776)
* i18n(pt-BR): translate 319 missing messages

Catch up with en.json: the new Preferences pages, assistant settings,
navigation preset help, the bug report form, history names, feature
folders, component links and copies, joint keys and the reference
geometry setting. Messages added to en.json after this branch started
stay in English for now.

* i18n(pt-BR): translate Delete as Apagar, not Excluir

"Excluir" (Latin excludere, to shut out) means to leave out or exclude;
"apagar" is the Portuguese for deleting, wiping something out. Use
apagar for every Delete already in the catalog (22 messages: commands,
the delete confirmation, rules, rows, sets, keys, crash reports), with
its forms: apaga, apagados, apagado(s), apagá-lo. The Delete key's name
stays as it is printed on the keyboard.

* Revert "i18n(pt-BR): translate Delete as Apagar, not Excluir"

This reverts commit 3bc73c5930.

Keep the catalog's existing word for Delete until the term is agreed
for the whole catalog in its own issue; the change can follow as a
separate PR.

* i18n(pt-BR): use Excluir in the five new Delete messages

Apply the review's suggestions: the new messages keep the catalog's
existing word for Delete.
2026-10-25 18:08:58 +00:00
raykholo 309eda87a2 Agent skill for reproducible bug reports (#3983) (#3984)
* claim: #3983

* Agent skill for reproducible bug reports (#3983)

skills/solidcraft-reporting has an agent gather the version, steps and
exact error, reproduce the problem in a fresh design (a command script or
one batch with rollback off), shrink it while it still fails, check the
numbers with measure and a screenshot, keep private designs out, search
for duplicates, and write the report in one shape: Reproduced on, Steps
with a minimal script, Expected, Actual, Attachments.

Listed in skills/README.md and docs/mcp.md; the bug template's steps
field points agents to it. The skill-name check now covers its files.

* Reporting skill: GitHub takes a design only as a .zip
2026-10-25 17:56:18 +00:00
null 00000111 c3fd7aa048 i18n: add missing Brazilian Portuguese (pt-BR) strings (#4136)
Adds the 8 messages missing from assets/locales/pt-BR.json (six help.solid.* tool descriptions and the agent off-thread setting). Catalog stays sorted; cargo xtask lists passes.
2026-10-25 17:56:15 +00:00
raykholo 2e6e05bc9a Sketch pattern edit shows its preview while dragging (#4412) (#4426) 2026-10-25 17:56:08 +00:00
Nathan Dane 7cea146438 Show scroll arrows on the theme row when more themes are hidden (#5846)
* Show scroll arrows on the theme row when more themes are hidden

* Fade the theme row edges and use the chevron icon for its scroll arrows

* Remove the outline left on the theme row arrows after a click

* Trim the theme row scroll arrow tests

* Drop the earlier theme row arrows' test and labels
2026-10-25 17:55:24 +00:00
JK3D 492bba9242 Control channel: write each reply in one piece (#6389) (#6390)
* claim: #6389

* perf: write each control reply to the socket in one piece

The control server wrote each reply with writeln!(out, "{reply}") on the unbuffered TcpStream.
Formatting a serde_json::Value hands the writer every string, number and punctuation mark
separately, so each became its own write system call. A reply of a few megabytes (component.list
on a design with thousands of components) took about half a second to send.

Every reply is now serialized to one buffer and written at once. Writing one large Value to a
loopback TcpStream: 1.79 MB took 1,052 ms with writeln! and 4.9 ms with one write_all.

The test counts the write calls for a 2000-component reply (one) and reads it back.
2026-10-25 17:55:14 +00:00
rbousigues88 9be14db8c0 Timeline: show only the active component's features (#2880) (#3543)
In an assembly the bottom timeline listed every feature of every component,
unreadable with a few dozen parts. While a component is active (or one is
picked in the browser), it shows only that component's features and those of
the components inside it. A toggle ahead of the features names the component
and switches to the whole design and back. The marker and drops map from the
shown features to the design's positions.

Tests: the_timeline_shows_only_the_active_components_features,
a_slot_among_the_shown_features_is_a_design_position.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:44:38 +00:00
raykholo b8c496f37a Gruvbox Dark and Dark Soft: readable selected tab labels (#4323) (#4324)
* claim: #4323

* Gruvbox Dark and Dark Soft: readable selected tab labels (#4323)

The Model tab and the shared tabs (Assemble, Insert, Inspect, Manage,
View) draw their selected label in selected_ink on accent_soft. Both
Gruvbox dark variants inherited the default dark selected_ink #9cc7f5,
which reaches only 4.48:1 on #48544f (Gruvbox Dark) and 4.13:1 on

Set selected_ink to the Gruvbox foreground #ebdbb2 in both files:
5.76:1 and 5.31:1. Tabs with their own accent keep their own ink.

A test checks the selected label of every workspace and contextual tab
against its soft background in Dark, Light and every built-in theme
file.
2026-10-25 17:43:55 +00:00
Colin Faulkingham aeb406188f Keep parameter rows stable while changing fields (#4461) (#4473)
* claim: #4461

* fix: keep parameter drafts stable between fields (#4461)
2026-10-25 17:43:30 +00:00
dudedagger42 ceed604498 Mesh export: a quality choice, so small round features get enough sides (#4747) (#4773)
Every STL, OBJ and 3MF export meshed each body with a chord tolerance of
2e-4 of its size, with no way to ask for finer: a 3 mm hole in a 200 mm
part came out with about 14 sides. file.export and file.save_mesh take
`quality`: standard (as before, the default), fine (5e-5) or finest
(1e-5), and Save As Mesh has a Quality row, remembered like its format.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:42:51 +00:00
raykholo 44bd29acc3 Hole: refuse a position that isn't on the face (#5024) (#5029)
* claim: #5024

* Hole: refuse a position that isn't on a face (#5024)

`solid.hole` took any `position` and drilled from the nearest face, so a
point 1 mm past a box's side was accepted and cut a hole across the edge.

A position given as a point (numbers or length expressions) must now lie on
a face of a body, within the same tolerance the plastic features use for
their placement (the check is shared with them). Holes at sketch points are
unchanged.

* Hole: a position over the mouth of a bore or opening of a planar face is on that face (#5024)

A counterbore is centred on an existing bore, in the plane of the face round it.

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 17:42:15 +00:00
HiroKenEz aa8e8cf69c Update Links: show which linked design it reads, off the window thread (#5329) (#5512)
* claim: #5329

* Update Links: show which linked design it reads, off the window thread (#5329)

Update Links read and rebuilt every linked design on the window thread, so the
window froze, with nothing on screen, until it finished. From the toolbar it now
runs as a file operation: off the window thread, with the overlay naming the
linked design being read (cover (2 of 3)), its rebuild step and progress, and
Cancel. The assembly stays as it was until the update is in, and it lands as one
undo step. The command reports each design through doc::progress::item.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:41:27 +00:00
Thodoris Tsagkogeorgas 669e14b3bf Preferences: search the installed plugins (#5585)
The Plugins page filters its list by name, id, description, and version.
2026-10-25 17:41:16 +00:00
github-actions[bot] 1ee65cf70a Regenerate generated files and fold pending files [skip ci] 2026-10-25 19:05:45 +00:00
raykholo 129364be5e Add the Sheet Metal Perforate dialog (#4379) (#4404) 2026-10-25 17:33:54 +00:00
github-actions[bot] 3be4727ca6 Regenerate generated files and fold pending files [skip ci] 2026-10-25 18:14:01 +00:00
github-actions[bot] a53eb927d1 Regenerate generated files and fold pending files [skip ci] 2026-10-25 18:07:53 +00:00
raykholo c3dd40405d Measure: open surface bodies report no volume or mass (#4257) (#4268) 2026-10-25 17:21:39 +00:00
bherbruck 5012f33c85 Section Analysis: bodies behind the cut no longer colour the cap (#6328) (#6728)
The cap pass redraws every back face behind a marked pixel with depth test
Always to put the section plane's depth there, and it also wrote colour.
The last back face drawn painted the cut, so a body behind the cut (one
inside another's hollow, an assembly's inner parts) showed through it in
its own colour and outline, like an X-ray. The pass now writes depth only;
the marking passes have already drawn the nearest back face's colour (the
same split as #4622).

Test: a_body_behind_the_cut_does_not_colour_the_cap in
crates/gpu/tests/capture.rs (a red body in a green box's hollow, 4 mm
behind the cut: the picture is the same with and without it, hatched and
plain, orthographic and perspective). On main 472 pixels differ.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:14:35 +00:00
raykholo eb37a82bc0 Finished sketches on a face stay visible: a dark halo under each curve (#4862) (#4956) 2026-10-25 17:08:58 +00:00
raykholo b1da434458 Graphics: choose the graphics card, honour WGPU_ADAPTER_NAME (#5986) (#6010) 2026-10-25 16:49:57 +00:00
bherbruck e1d99ef0ce Section Analysis: the pointer over a cut no longer picks what the cut hides (#6328) (#6722)
The viewport's picker took the nearest face along the ray whatever the
section removed. Over a cut it hovered the cut-away face in front of the
plane (the outer wall of a sectioned hub, say); the shader clipped that
face's tint, but its outline, drawn dimmed where the model hides it, ran
behind the cap and showed through it. While orbiting with the pointer on
the model the hovered face changed from frame to frame, so lines and
patches of the far side came and went over the hatch.

The picker now does what view.pick already does: faces, edges and corners
on the removed side are skipped (the ray walks past them), and where the
plane crosses a body nearer than any kept face, the cap hides the faces,
edges and corners behind it and nothing is hovered there. The walk that
tells whether the plane is inside a body moves into
view_pick::body_cut_at so both pickers share it.

Test: view_pick_tests::the_mouse_picker_stops_at_a_section_cap (a box
with a pocket, cut through the pocket: over the cut material nothing is
picked, over the pocket its floor, and with the section cleared the top
face). It fails on main, which picks the removed top face.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:37:31 +00:00
github-actions[bot] 4f2c133d01 Regenerate generated files and fold pending files [skip ci] 2026-10-25 17:16:02 +00:00
Ahmad Khan 0f90fb7e8d Edge flange: material outside and tangent bend positions (#5941) (#6060)
* claim: #5938

* Edge flange: height measured from the outer sharp, the inner sharp or the bend tangent (#5938)

An edge flange feature takes height_from (outer, inner or tangent) in sheet.flange and Height From in the Flange dialog, saved only when it is not the outer sharp. A height shorter than the bend takes is refused with the smallest height that fits.

* Edge flange: material outside and tangent bend positions (#5941)

An edge flange's position can now be material_outside (the inner virtual sharp on the edge) or tangent (the bend's outer face tangent to the edge's face), in sheet.flange and the Flange dialog. The bend starting at the edge was already the outside position, which now also answers to bend_outside.
2026-10-25 16:25:18 +00:00
amolinari-autotex 52e5bc114e Display cosmetic internal and external threads (#2367) (#2868)
* claim: #2367

* feat: distinguish cosmetic threads in shaded views

* test: keep cosmetic thread coverage compatible with current sections and assemblies
2026-10-25 14:53:24 +00:00
raykholo 4cd9a4f4d4 Web app: open a design from a link, and an embed mode (#6252) (#6253)
* claim: #6252

* Open hosted designs in the web app and add an embed layout

Fixes #6252.

Fetch HTTPS design links with bounded reads and open them as unsaved copies.
Keep model panels and joint controls in embeds without browser persistence,
application chrome, or unload prompts. Add a full-app link and iframe docs.

Cover query parsing, hostile URLs, size limits, and the compact layout.

* Translate the linked-design messages into Japanese and Chinese
2026-10-25 14:40:01 +00:00
CamWight 4577dffb36 Viewport: ambient occlusion no longer shows ghost copies of the model (#4167) (#4251)
* claim: #4167

* Viewport: ambient occlusion no longer shows ghost copies of the model (#4167)

The AO shader used the same 32 sample offsets at every pixel, and nothing smoothed the
result. On a flat face, each sample darkened its own shifted copy of a nearby part's outline.
Where the radius is large on screen (a big design seen close up), those copies show as faint
separate ghosts of the part. Where it is small, they pack into stepped bands along contacts
(#3407).

The sample pattern is now turned by a different angle at each pixel, using interleaved
gradient noise of the pixel position (Jimenez 2014). The copies break up into an even fine
grain, which 2x supersampling smooths further. The cost is per pixel only (one noise value,
one sine and one cosine). The 32-sample loop, the depth prepass and AO off are unchanged.

Test: ambient_occlusion_draws_no_copies_of_a_part (viewport.rs) renders a block on a plate
from above with AO off and on. AO must still darken beside the block, the plate must stay
clear beyond the radius, and the profile beside the block's straight side must have no
copy-sized steps (0.0144 before, 0.0026 after; limit 0.005). The test-only offscreen helper
gains an AO radius, like render_offscreen_ssaa.

Fixes #4167

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:57:29 +00:00
249 changed files with 11155 additions and 1085 deletions
+1
View File
@@ -19,6 +19,7 @@ body:
label: Steps to reproduce
description: |
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
placeholder: |
1. Sketch a circle on the XY plane
2. Extrude it 10 mm
+11 -8
View File
@@ -51,10 +51,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
@@ -62,10 +62,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
@@ -158,6 +158,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
| assets/locales/nb.json | SolidCraft contributors | Original Norwegian Bokmål translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
@@ -199,6 +200,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
Generated
+18 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4240,11 +4240,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.22.1"
version = "0.23.0"
[[package]]
name = "solidcraft-cli"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"image",
"log",
@@ -4256,7 +4256,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"proc-macro2",
"quote",
@@ -4265,7 +4265,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"log",
"serde",
@@ -4282,7 +4282,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"log",
"schemars",
@@ -4302,7 +4302,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"serde",
"serde_json",
@@ -4311,7 +4311,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"image",
"log",
@@ -4331,7 +4331,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4355,7 +4355,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"log",
"proptest",
@@ -4375,7 +4375,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4384,7 +4384,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"image",
"moxcms",
@@ -4398,7 +4398,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"serde",
"serde_json",
@@ -4409,14 +4409,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"arboard",
"egui",
@@ -4445,7 +4445,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"eframe",
"egui",
@@ -4454,6 +4454,7 @@ dependencies = [
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -5982,7 +5983,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"proc-macro2",
"serde_json",
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.22.1"
version = "0.23.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+2 -2
View File
@@ -46,9 +46,9 @@
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
</p>
![SolidCraft 0.8: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo-0.8.gif)
![SolidCraft: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo.gif)
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
+2
View File
@@ -22,6 +22,7 @@ solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
url = "2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
@@ -34,5 +35,6 @@ web-sys = { version = "0.3", features = [
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
] }
+136
View File
@@ -0,0 +1,136 @@
//! Bounded HTTPS downloads, handed to the normal design-open command as unsaved copies.
use std::{cell::RefCell, rc::Rc};
use js_sys::{Reflect, Uint8Array};
use solidcraft_engine::io::vfs;
use solidcraft_ui_egui::{SolveApp, ui_kit};
use wasm_bindgen::{JsCast as _, JsValue};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use crate::{check_download_size, design_url};
type Reply = Result<(String, Vec<u8>), String>;
#[derive(Default)]
pub struct Download {
reply: Rc<RefCell<Option<Reply>>>,
pending: Option<(String, Vec<u8>)>,
}
impl Download {
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
let download = Self::default();
if let Some(url) = url {
let url = url.to_owned();
let slot = download.reply.clone();
let ctx = ctx.clone();
spawn_local(async move {
*slot.borrow_mut() = Some(fetch(&url).await);
ctx.request_repaint();
});
}
download
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
if let Some(reply) = self.reply.borrow_mut().take() {
match reply {
Ok(file) => self.pending = Some(file),
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
}
}
if self.pending.is_none() || app.file_picker_busy() {
return;
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
ui.horizontal(|ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
answer = Some(true);
}
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
answer = Some(false);
}
});
});
match answer {
Some(true) => {}
Some(false) => {
self.pending = None;
return;
}
None if closed => {
self.pending = None;
return;
}
None => return,
}
}
let Some((name, bytes)) = self.pending.take() else { return };
// The extension selects the design loader even when the URL has no extension.
let path = "/linked/design.solidcraft";
vfs::insert(path, bytes);
app.open_path(path);
if app.session.path.as_deref() == Some(path) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
} else {
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
}
let _ = vfs::remove(path);
}
}
async fn fetch(value: &str) -> Reply {
let url = design_url(value)?;
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
let window = web_sys::window().ok_or("No browser window")?;
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
design_url(&response.url()).map_err(|_| "Design redirected to a URL that does not use HTTPS")?;
if !response.ok() {
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
}
let length = response
.headers()
.get("Content-Length")
.map_err(|_| "Cannot read download size")?
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
.transpose()?;
if let Some(length) = length {
check_download_size(0, length)?;
}
let stream = response.body().ok_or("Download has no body")?;
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
let result = read(&reader).await;
if result.is_err() {
let _ = JsFuture::from(reader.cancel()).await;
}
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
// can describe compressed bytes, so the streamed limit also checks the decoded size.
let bytes = result?;
Ok((name, bytes))
}
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
let mut bytes = Vec::new();
// Bound even a stream which keeps returning empty chunks.
for _ in 0..1_000_000 {
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
return Ok(bytes);
}
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
check_download_size(bytes.len(), u64::from(chunk.length()))?;
bytes.extend_from_slice(&chunk.to_vec());
}
Err("Download returned too many chunks".into())
}
+159 -13
View File
@@ -14,6 +14,8 @@ mod disk;
mod files;
#[cfg(target_arch = "wasm32")]
mod install;
#[cfg(target_arch = "wasm32")]
mod linked;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
@@ -25,6 +27,61 @@ fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
ctrl && key == "Tab"
}
#[cfg(any(target_arch = "wasm32", test))]
fn query_flag(query: &str, name: &str) -> bool {
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
}
/// Launch options use exact query keys; URL values are decoded only once.
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Default, Debug)]
struct Launch {
open: Option<String>,
embed: bool,
}
#[cfg(any(target_arch = "wasm32", test))]
impl Launch {
fn parse(query: &str) -> Self {
let mut launch = Self::default();
let mut embed = false;
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
match key.as_ref() {
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
"embed" => embed = true,
_ => {}
}
}
launch.embed = embed && launch.open.is_some();
launch
}
fn persists(&self) -> bool {
!self.embed
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn design_url(value: &str) -> Result<url::Url, String> {
if value.len() > 16_384 {
return Err("Design URL is too long".into());
}
let url = url::Url::parse(value).map_err(|_| "Invalid design URL")?;
if url.scheme() != "https" || url.host_str().is_none() || !url.username().is_empty() || url.password().is_some() {
return Err("Design links must use HTTPS without credentials".into());
}
Ok(url)
}
#[cfg(any(target_arch = "wasm32", test))]
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
@@ -112,6 +169,9 @@ mod web {
struct Shell {
app: SolveApp,
launch: crate::Launch,
linked: crate::linked::Download,
full_url: Option<String>,
api: Api,
files: Rc<Files>,
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
@@ -137,6 +197,9 @@ mod web {
impl Shell {
/// Files from the computer, writes to store, messages and the autosave.
fn files_tick(&mut self, ctx: &egui::Context) {
if !self.launch.persists() {
return;
}
if self.files.ctx.borrow().is_none() {
*self.files.ctx.borrow_mut() = Some(ctx.clone());
}
@@ -526,19 +589,42 @@ mod web {
if let Some(delay) = self.wheel_gestures.update(raw, now) {
ctx.request_repaint_after(delay);
}
files::take_drops(&self.files, raw);
if self.launch.persists() {
files::take_drops(&self.files, raw);
} else {
raw.dropped_files.clear();
}
// Before the app's own hook, which adds the control channel's events.
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
self.app.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.linked.tick(&mut self.app, ui.ctx());
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
if self.launch.embed {
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
{
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
}
});
}
self.app.ui(ui);
let ctx = ui.ctx().clone();
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
if self.launch.persists() {
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
}
self.watch_text_for_font(&ctx);
}
fn persist_egui_memory(&self) -> bool {
self.launch.persists()
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.launch.persists() {
return;
}
// Persist the user's own language, not a session-only English fallback.
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
@@ -557,15 +643,15 @@ mod web {
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
fn webgl() -> bool {
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
let q = query();
if q.contains("webgl") {
if crate::query_flag(&q, "webgl") {
if let Some(s) = &storage {
let _ = s.set_item(WEBGL_KEY, "1");
}
return true;
}
if q.contains("webgpu") {
if crate::query_flag(&q, "webgpu") {
if let Some(s) = &storage {
let _ = s.remove_item(WEBGL_KEY);
}
@@ -591,7 +677,10 @@ mod web {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
ask_before_unload(&window);
let launch = crate::Launch::parse(&query());
if launch.persists() {
ask_before_unload(&window);
}
leave_tab_switching_to_browser(&window);
let Some(document) = window.document() else { return };
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
@@ -611,10 +700,12 @@ mod web {
Box::new(move |cc| {
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
let files = Rc::new(Files::default());
files::start(&files);
if launch.persists() {
files::start(&files);
}
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
let (restorable, restorer) = (files.clone(), files.clone());
let services = Services {
let services = if launch.embed { Services::default() } else { Services {
// Open shows the browser's open dialog for a design where it has one
// (the file keeps its handle, so Save writes back to it); otherwise,
// and for inserting STEP or meshes, the browser's files window.
@@ -648,26 +739,27 @@ mod web {
browser_files: Some(Box::new(move || shown.panel.set(true))),
install_app: Some(crate::install::service(&cc.egui_ctx)),
..Default::default()
};
}};
// Thumbnails of saves and start-page cards.
solidcraft_ui_egui::gpu::install_cpu_picture_renderer();
let (sender, receiver) = std::sync::mpsc::channel();
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.embedded = launch.embed;
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
// eframe's web painter has no stencil buffer.
app.set_wgpu(rs, DEPTH_BITS, 0, 1);
}
if query().contains("sample") {
if launch.open.is_none() && crate::query_flag(&query(), "sample") {
if let Err(e) = app.session.run_script(&solidcraft_engine::sample::script()) {
app.set_status(format!("sample: {e}"), true);
}
app.session.doc_mut().name = "Sample Plate".into();
app.session.mark_saved();
}
if !query().contains("sample") {
if launch.open.is_none() && !crate::query_flag(&query(), "sample") {
// The empty design takes Units of new designs (#1205). The sample
// part is built in millimetres and is left that way.
solidcraft_ui_egui::prefs::apply_new_design(&mut app);
@@ -676,8 +768,20 @@ mod web {
}
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
let font_language = app.preferences.language.clone();
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|mut url| {
url.set_query(None);
url.set_fragment(None);
if let Some(open) = &launch.open {
url.query_pairs_mut().append_pair("open", open);
}
url.to_string()
});
Ok(Box::new(Shell {
app,
launch,
linked,
full_url,
api,
files,
autosaved_at: f64::NEG_INFINITY,
@@ -721,6 +825,48 @@ fn main() {
mod tests {
use super::*;
#[test]
fn link_query_decodes_once_and_embed_disables_persistence() {
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
assert!(query_flag("?embed&webgl", "webgl"));
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
assert!(launch.embed);
assert!(!launch.persists());
assert!(!Launch::parse("?embed").embed);
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
assert!(Launch::parse("?open=https://example.org/a").persists());
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
}
#[test]
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
assert!(design_url("https://example.org/arm.solidcraft?download=1").is_ok());
for value in [
"http://example.org/a",
"data:application/json,{}",
"blob:https://example.org/a",
"javascript:alert(1)",
"/arm.solidcraft",
"https://user:secret@example.org/a",
"",
] {
assert!(design_url(value).is_err(), "{value}");
}
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384))).is_err());
assert!(design_url("http://redirect.example.org/a").is_err());
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
assert!(check_download_size(0, cap as u64).is_ok());
assert!(check_download_size(0, cap as u64 + 1).is_err());
assert!(check_download_size(cap - 2, 2).is_ok());
assert!(check_download_size(cap - 2, 3).is_err());
assert!(check_download_size(0, u64::MAX).is_err());
assert!(check_download_size(cap, 1).is_err());
assert!(check_download_size(usize::MAX, 0).is_err());
}
#[test]
fn only_ctrl_tab_is_left_to_the_browser() {
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
+40 -4
View File
@@ -127,7 +127,7 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
OPEN.fetch_sub(1, Ordering::Relaxed);
let mut refused = stream;
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
continue;
}
let tx = tx.clone();
@@ -174,6 +174,15 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
}
}
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
/// of its pieces apart, a system call for every string, number and comma: half a second for a
/// design's component tree when it has thousands of components.
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
line.push(b'\n');
out.write_all(&line)
}
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
@@ -184,7 +193,7 @@ fn unauthorized() -> Value {
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
use std::io::Read;
let _ = writeln!(out, "{}", unauthorized());
let _ = send(out, &unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
@@ -381,7 +390,7 @@ fn serve_with(
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
Ok(None) => break,
Err(e) => {
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
let _ = send(&mut out, &json!({"ok": false, "error": e.to_string()}));
break;
}
};
@@ -463,7 +472,7 @@ fn serve_with(
json!({"ok": false, "error": format!("bad JSON: {e}")})
}
};
if writeln!(out, "{reply}").is_err() {
if send(&mut out, &reply).is_err() {
break;
}
if bad >= MAX_BAD_LINES {
@@ -750,4 +759,31 @@ mod tests {
let mut r = BufReader::with_capacity(64, text.as_slice());
assert!(read_line(&mut r, 1000).is_err());
}
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
/// thousands of components took half a second to send), and reads back as it was.
#[test]
fn a_large_reply_is_written_at_once() {
struct Counted {
writes: usize,
bytes: Vec<u8>,
}
impl Write for Counted {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writes += 1;
self.bytes.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
let mut out = Counted { writes: 0, bytes: Vec::new() };
send(&mut out, &reply).unwrap();
assert_eq!(out.writes, 1);
assert_eq!(out.bytes.last(), Some(&b'\n'));
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
}
}
+16 -3
View File
@@ -335,6 +335,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
let future = dialog.save_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
window_system: window_system_now(),
http_get: Some(std::sync::Arc::new(http_get)),
@@ -802,7 +803,19 @@ fn main() -> eframe::Result {
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
lost_at: None,
});
if !safe_mode {
app.preferences.graphics_adapter =
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
}
if let Some(rs) = &cc.wgpu_render_state {
app.services.graphics_adapters = rs
.available_adapters
.iter()
.map(|adapter| {
let info = adapter.get_info();
(info.name.clone(), format!("{} ({:?}, {:?})", info.name, info.device_type, info.backend))
})
.collect();
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
@@ -935,9 +948,9 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
}
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
startup::configure_adapter(setup, cpu, environment, adapter);
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
let base = setup.device_descriptor.clone();
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
+84
View File
@@ -129,6 +129,48 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
})
}
/// Adapter properties used to choose a card without opening a device.
#[derive(Clone, Debug)]
pub struct AdapterChoice {
pub name: String,
pub compatible: bool,
}
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
/// environment override follows wgpu's case-insensitive substring convention.
/// No match leaves selection to the normal wgpu request.
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
if let Some(name) = environment.filter(|s| !s.is_empty()) {
let name = name.to_lowercase();
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
return Some(index);
}
}
if let Some(name) = preference.filter(|s| !s.is_empty())
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
{
return Some(index);
}
None
}
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
let choices: Vec<_> = adapters
.iter()
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
.collect();
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
}));
}
}
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
@@ -249,6 +291,48 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
mod tests {
use super::*;
fn cards() -> Vec<AdapterChoice> {
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
}
#[test]
fn adapter_names_environment_override_and_missing_card_fallback() {
let cards = cards();
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
}
#[test]
fn automatic_adapter_leaves_selection_to_wgpu() {
let mut cards = cards();
assert_eq!(choose_adapter(&cards, None, None), None);
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
cards[2].compatible = false;
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
assert_eq!(choose_adapter(&[], None, None), None);
}
#[test]
fn automatic_adapter_preserves_the_previous_request_configuration() {
use eframe::egui_wgpu::WgpuSetupCreateNew;
let previous = WgpuSetupCreateNew::without_display_handle();
for preference in [None, Some(String::new())] {
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, None, preference);
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
assert_eq!(setup.power_preference, previous.power_preference);
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
}
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
}
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
+3 -3
View File
@@ -152,11 +152,11 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
pub(crate) const AXIS_RADIUS: f32 = 15.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
let col = if d.x != 0.0 {
t.triad[0]
t.axis_x
} else if d.y != 0.0 {
t.triad[1]
t.axis_y
} else {
t.triad[2]
t.axis_z
};
let positive = !name.starts_with('-');
let light = t.panel.r() > 128;
+4
View File
@@ -1759,6 +1759,8 @@
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
"ui.findInTimeline": "Najít na časové ose",
"ui.findInWindow": "Najít v okně",
"ui.fine": "Jemná",
"ui.finest": "Nejjemnější",
"ui.finish_sketch": "Dokončit náčrt",
"ui.fit": "Přizpůsobit",
"ui.fix_unfix": "Zafixovat/uvolnit",
@@ -1986,6 +1988,7 @@
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
"ui.properties": "Vlastnosti",
"ui.pull_direction": "Směr vyjímání",
"ui.quality": "Kvalita",
"ui.quantity": "Počet",
"ui.quantity_2": "Počet 2",
"ui.quit": "Ukončit",
@@ -2080,6 +2083,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Rozdělit těleso",
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
"ui.standard": "Standardní",
"ui.start": "Start",
"ui.start_over": "Začít znovu",
"ui.start_page": "Úvodní stránka",
+389 -6
View File
@@ -5,7 +5,9 @@
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Vedhæft dit skærmbillede: Kopiér og indsæt det, eller gem og upload PNG-filen.",
"2D": "2D",
"3. Click Submit new issue on GitHub to send your report.": "3. Klik på Submit new issue på GitHub for at sende rapporten.",
"A Single hole snaps to the middle of its face and to the centres of its round edges": "Et enkelt hul snapper til fladens midtpunkt og centrene af dens cirkulære kanter",
"A new configuration, a copy of the active one": "Opret en ny konfiguration som en kopi af den aktive",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "Et placeret enkelt hul har et håndtag til at trække i. Hvis fladen vælges igen, flyttes hullet dertil i stedet for at blive fjernet",
"ARRANGE": "ARRANGER",
"About SolidCraft": "Om SolidCraft",
"Acceleration": "Acceleration",
@@ -14,23 +16,31 @@
"Active": "Aktiv",
"Active rule": "Aktiv regel",
"Add": "Tilføj",
"Add Column": "Tilføj kolonne",
"Add Key": "Tilføj animationspunkt",
"Add Mid Radius": "Tilføj mellemradius",
"Add Row": "Tilføj række",
"Add a tool": "Tilføj et værktøj",
"Add condition": "Tilføj betingelse",
"Add or edit rule": "Tilføj eller redigér regel",
"Add to New Folder": "Føj til ny mappe",
"Advanced": "Avanceret",
"Against": "Mod",
"Alignment": "Justering",
"All Lines": "Alle linjer",
"All Sketch Points": "Alle skitsepunkter",
"All components": "Alle komponenter",
"All open designs": "Alle åbne designs",
"Allowed expressions, separated by semicolons": "Tilladte udtryk adskilt af semikolon",
"Also deletes": "Sletter også",
"Anchor": "Forankring",
"Angle 2": "Vinkel 2",
"Animate": "Animér",
"App/system information and command settings are included. Review private details before sharing.": "Oplysninger om programmet, systemet og kommandoindstillingerne er inkluderet. Gennemgå private oplysninger, før du deler rapporten.",
"Appearance name": "Navn på udseende",
"Applied Radius": "Anvendt radius",
"Apply To": "Anvend på",
"Apply library appearance": "Anvend udseende fra biblioteket",
"Apply to selection": "Anvend på markering",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Anvend først dit udseende. Eksporterer tildelte udseender og materialer oprettet i dette design.",
"Arc length": "Buelængde",
@@ -41,13 +51,22 @@
"BLEND SURFACE": "OVERGANGSFLADE",
"BOUNDARY SURFACE": "AFGRÆNSET OVERFLADE",
"BROWSER": "MODELTRÆ",
"Bar": "Stang",
"Bend Edges": "Bøjningskanter",
"Bend Line Position": "Bukkelinjens placering",
"Bend Position": "Bukningens placering",
"Bend Radius": "Bukkeradius",
"Bend Table": "Bukketabel",
"Bends": "Bukninger",
"Bodies": "Emner",
"Bodies (optional)": "Legemer (valgfrit)",
"Body": "Emne",
"Body (last if none)": "Legeme (det sidste, hvis intet er valgt)",
"Body to split": "Legeme, der skal opdeles",
"Body, or faces to take out": "Legeme eller flader, der skal fjernes",
"Bottom Left": "Nederst til venstre",
"Bottom Middle": "Nederst i midten",
"Bottom Right": "Nederst til højre",
"Boundary": "Afgrænsning",
"Bounding box": "Omsluttende boks",
"CHECK": "KONTROLLÉR",
@@ -59,32 +78,45 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV eller JSON: Opdaterer eksisterende parametre og tilføjer nye",
"Capture All Positions": "Registrér alle positioner",
"Capture Pos.": "Registrér pos.",
"Catch Depth": "Låsedybde",
"Catch Direction": "Indgrebsretning",
"Catch Length": "Låselængde",
"Cells": "Celler",
"Center X": "Centrum X",
"Center Y": "Centrum Y",
"Center of mass": "Massecentrum",
"Centre to centre": "Centrum til centrum",
"Centroid": "Tyngdepunkt",
"Chamfer Type": "Affasningstype",
"Channel": "U-profil",
"Check collisions": "Kontrollér kollisioner",
"Checked": "Kontrolleret",
"Choose Variable radius or Spline in Type.": "Vælg variabel radius eller spline under Type.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Vælg et nyt plan. Skitsegeometri og mål bevarer deres todimensionelle koordinater.",
"Choose…": "Vælg…",
"Circle center": "Cirklens centrum",
"Circular": "Cirkulær",
"Circumference": "Omkreds",
"Clockwise": "Med uret",
"Close draft": "Luk kladde",
"Collapse": "Fold sammen",
"Collisions": "Kollisioner",
"Colour": "Farve",
"Command": "Kommando",
"Comment": "Kommentar",
"Component 1": "Komponent 1",
"Component 2": "Komponent 2",
"Component1": "Komponent1",
"Components": "Komponenter",
"Config Table": "Konfig.tabel",
"Configuration": "Konfiguration",
"Configuration rules": "Konfigurationsregler",
"Contact": "Kontakt",
"Contact Sets": "Kontaktsæt",
"Contact sets": "Kontaktsæt",
"Continue on GitHub": "Fortsæt på GitHub",
"Continuity": "Kontinuitet",
"Control Radius": "Styreradius",
"Copy Error Message": "Kopiér fejlmeddelelse",
"Copy Warning Message": "Kopiér advarselsmeddelelse",
"Copy all licences": "Kopiér alle licenser",
@@ -93,9 +125,14 @@
"Copy screenshot": "Kopiér skærmbillede",
"Corner Relief": "Hjørneaflastning",
"Corner Relief Shape": "Hjørneaflastningens form",
"Corner radius": "Hjørneradius",
"Counterbore depth": "Dybde af cylindrisk forsænkning",
"Counterbore Ø": "Cylindrisk forsænkning Ø",
"Countersink angle": "Vinkel på konisk forsænkning",
"Countersink Ø": "Konisk forsænkning Ø",
"Create Hole": "Opret hul",
"Create Issue — review draft": "Opret issue — gennemgå kladde",
"Create Issue…": "Opret fejlrapport…",
"Cross curves": "Tværkurver",
"Cube": "Terning",
"Current orthographic view (1:1 mm)": "Aktuel visning uden perspektiv (1:1 mm)",
@@ -103,22 +140,33 @@
"Curv. Comb": "Krumningskam",
"Curvature (G2)": "Krumningskontinuitet (G2)",
"Curvature change (G3)": "Ændring i krumning (G3)",
"Custom": "Brugerdefineret",
"Customize Toolbar": "Tilpas værktøjslinje",
"Customize…": "Tilpas…",
"Cylindrical face": "Cylindrisk flade",
"Delete": "Slet",
"Delete Selected Features…": "Slet valgte features…",
"Delete Selected Folders": "Slet valgte mapper",
"Delete all": "Slet alle",
"Delete rule": "Slet regel",
"Delete the row": "Slet rækken",
"Delete the set": "Slet sættet",
"Density": "Densitet",
"Dependents": "Afhængige",
"Describe the problem you saw": "Beskriv problemet, du oplevede",
"Describe what happened before continuing.": "Beskriv, hvad der skete, før du fortsætter.",
"Designs you open or save appear here.": "Designs, du åbner eller gemmer, vises her.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestemmes af Til objekt. Vælg Vinkel for at ændre vinkeludstrækningen.",
"Diameter": "Diameter",
"Direction": "Retning",
"Discard": "Kassér",
"Distance": "Afstand",
"Distance 1": "Afstand 1",
"Do not export hidden edges": "Eksportér ikke skjulte kanter",
"Don't ask again": "Spørg ikke igen",
"Double-click to rename": "Dobbeltklik for at omdøbe",
"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": "Dobbeltklik på den midterste museknap i visningen: Hjem (hjemmevisningen, der indrammer det viste), Tilpas (indrammer det viste og bevarer orienteringen) eller ingen handling",
"Drag to move the hole; click to remove it": "Træk for at flytte hullet; klik for at fjerne det",
"Drive Joint": "Bevæg led",
"Duration": "Varighed",
"Edges": "Kanter",
@@ -126,80 +174,113 @@
"Edit Profile Sketch": "Redigér profilskitse",
"Enable rule": "Aktivér regel",
"Enabled": "Aktiveret",
"End Radius": "Slutradius",
"End bulge": "Udbuling ved slutningen",
"End continuity": "Overgang ved slutningen",
"Expand": "Fold ud",
"Expected result (optional)": "Forventet resultat (valgfrit)",
"Explode": "Adskil",
"Explode Text": "Opløs tekst",
"Export Animation…": "Eksportér animation…",
"Export Bend Table…": "Eksportér bøjningstabel…",
"Export CSV…": "Eksportér CSV…",
"Export Current View as DXF": "Eksportér aktuel visning som DXF",
"Export Current View as DXF…": "Eksportér aktuel visning som DXF…",
"Export DXF": "Eksportér DXF",
"Export DXF…": "Eksportér DXF…",
"Export JSON…": "Eksportér JSON…",
"Export design library...": "Eksportér designbibliotek...",
"Export hidden edges as dashed lines": "Eksportér skjulte kanter som stiplede linjer",
"Exported by SolidWorks, which usually saves Y up": "Eksporteret af SolidWorks, som normalt gemmer med Y opad",
"Expression": "Udtryk",
"Extend to meet": "Forlæng, til de mødes",
"Extents": "Udstrækning",
"FILES": "FILER",
"FLAT": "FLAD",
"Face": "Flade",
"Face Appearance": "Fladens udseende",
"Face attachment lost": "Forbindelsen til fladen er mistet",
"Face center": "Fladens midtpunkt",
"Faces": "Flader",
"Faces / edges / vertices": "Flader / kanter / hjørnepunkter",
"Faces to split": "Flader, der skal opdeles",
"Fastener": "Befæstigelse",
"Favorites": "Favoritter",
"Favourite": "Favorit",
"Feature": "Feature",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Filer fra mange CAD-programmer har Y opad og åbnes derfor liggende på ryggen her (Z opad).",
"Filter": "Filter",
"Filter…": "Filtrér…",
"Find in Browser": "Find i modeltræ",
"Finish": "Afslut",
"First plane": "Første plan",
"Fit and G2 corners require Constant radius.": "Tilpasning og G2-hjørner kræver konstant radius.",
"Fixed Face": "Fast flade",
"Flange thickness": "Flangetykkelse",
"Flat Pattern": "Flad afvikling",
"Flat size": "Bredde mellem flader",
"Follow camera (Look At sketches)": "Følg kamera (Se på skitser)",
"Follow the camera (Look At sketches)": "Følg kameraet (Se på skitser)",
"Font": "Skrifttype",
"From": "Fra",
"From Bodies": "Fra emner",
"From edges": "Fra kanter",
"Full Length": "Fuld længde",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-hjørner kræver konstant radius og præcis tilpasning. Konstruktion af G2-hjørner understøttes endnu ikke.",
"Gap": "Spalte",
"Grid": "Gitter",
"HISTORY": "HISTORIK",
"Height": "Højde",
"Hidden edges": "Skjulte kanter",
"History layout": "Historiklayout",
"History names": "Navne i historikken",
"Hole (optional)": "Hul (valgfrit)",
"Holes": "Huller",
"Holes (edges on them)": "Huller (kanterne omkring dem)",
"Home view": "Hjemmevisning",
"Horiz./Vert.": "Vandr./Lodr.",
"I-Beam": "I-profil",
"IMAGES": "BILLEDER",
"Import…": "Importér…",
"In This Design": "I dette design",
"In place (surfaces)": "På stedet (overflader)",
"In plane": "I planet",
"Inside": "Indvendigt",
"Instant": "Øjeblikkelig",
"Interface font": "Skrifttype i brugergrænsefladen",
"Interferes": "Overlapper",
"Invert": "Invertér",
"Isocurve": "Isokurve",
"Issue title": "Titel på issue",
"Items": "Elementer",
"JOINTS": "LED",
"Joint Origin1": "Lednulpunkt1",
"Keep (points on it)": "Behold (punkter på fladen)",
"Keep Tools": "Behold værktøjsemner",
"Keep of the other surface": "Behold fra den anden overflade",
"Key a joint's value at this step": "Indsæt en nøgle for leddets værdi på dette trin",
"Keys": "Nøgler",
"L-Angle": "L-profil",
"LIBRARY": "BIBLIOTEK",
"LINKS": "LINKS",
"Later": "Senere",
"Leave it": "Lad det være",
"Left Middle": "Til venstre i midten",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Ældre fladetilknytning. Ny tilknytning til en certificeret flade giver en præcis tilknytning.",
"Length": "Længde",
"Length units": "Længdeenheder",
"Letter Spacing": "Tegnafstand",
"Library": "Bibliotek",
"Library file": "Biblioteksfil",
"Limits": "Grænser",
"Line Spacing": "Linjeafstand",
"Line Type": "Linjetype",
"Lines": "Linjer",
"Listing the installed fonts…": "Henter listen over installerede skrifttyper…",
"Load library": "Indlæs bibliotek",
"Local library": "Lokalt bibliotek",
"Loop": "Løkke",
"M5 or M5x0.5": "M5 eller M5x0.5",
"MANAGE": "ADMINISTRÉR",
"MATERIALS": "MATERIALER",
"MODEL": "MODEL",
@@ -210,18 +291,25 @@
"Matched restrictions check the final values.": "Matchede begrænsninger kontrollerer de endelige værdier.",
"Material": "Materiale",
"Max": "Maks.",
"Maximum": "Maksimum",
"Mesh body": "Meshlegeme",
"Mid Radius": "Mellemradius",
"Middle double-click": "Dobbeltklik med midterste museknap",
"Min": "Min.",
"Minimum": "Minimum",
"Mirror Plane": "Spejlingsplan",
"Mitre": "Gering",
"Model Parameters": "Modelparametre",
"Modeled": "Modelleret",
"More toolbar panels": "Flere værktøjspaneler",
"Motion": "Bevægelse",
"Mouse gestures": "Musebevægelser",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Musebevægelser virker kun med SolidWorks-musetilstanden (musehjulet roterer visningen, Ctrl + musehjul panorerer).",
"Move / Copy": "Flyt / Kopiér",
"Move Type": "Flytningstype",
"Move down": "Flyt ned",
"Move earlier": "Flyt tidligere",
"Move holes by clicking or dragging": "Flyt huller ved at klikke eller trække",
"Move later": "Flyt senere",
"Move up": "Flyt op",
"Name": "Navn",
@@ -235,17 +323,25 @@
"No matching appearance presets.": "Ingen matchende udseendeforudindstillinger.",
"No matching appearances": "Ingen matchende udseender",
"No visible bodies or sketches to export.": "Der er ingen synlige emner eller skitser at eksportere.",
"Nominal diameter": "Nominel diameter",
"None": "Ingen",
"Not available yet": "Endnu ikke tilgængelig",
"Notch": "Udskæring",
"OUTPUT": "OUTPUT",
"Of": "Af",
"On your computer": "På din computer",
"Opacity": "Opacitet",
"Open GitHub draft": "Åbn GitHub-kladde",
"Open library...": "Åbn bibliotek...",
"Open local libraries in the desktop app. You can export this design's library here.": "Åbn lokale biblioteker i desktopappen. Du kan eksportere dette designs bibliotek her.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Parametrisk 3D-CAD i Rust med åben kildekode. Licenseret under MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Åbner din browser og kopierer rapportteksten, så du kan indsætte den på GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Åbner din browser. Gennemgå og indsend rapporten på GitHub.",
"Operator": "Operator",
"Orbit (right drag)": "Rotér visningen (træk med højre museknap)",
"Other side": "Anden side",
"Other surface (mutual trim)": "Anden overflade (trim begge)",
"Outer radius": "Udvendig radius",
"Outside": "Udvendigt",
"Overall length": "Samlet længde",
"Param. Graph": "Param.graf",
@@ -258,8 +354,11 @@
"Pick the tool bodies": "Vælg værktøjsemnerne",
"Picked": "Valgt",
"Picked point": "Valgt punkt",
"Pitch": "Stigning",
"Pivot": "Drejepunkt",
"Place at the origin instead": "Placér ved nulpunktet i stedet",
"Plastic Rules": "Plastregler",
"Play the joint's motion in the view": "Afspil leddets bevægelse i visningen",
"Playback": "Afspilning",
"Plot": "Diagram",
"Position": "Position",
@@ -267,22 +366,29 @@
"Position (G0)": "Positionskontinuitet (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Position langs kanten: 0 ved den ene ende, 1 ved den anden",
"Preferences › Design turns the offer back on": "Indstillinger › Design slår tilbuddet til igen",
"Preview": "Forhåndsvisning",
"Preview Row": "Forhåndsvis række",
"Preview leaves the current design unchanged.": "Forhåndsvisning ændrer ikke det aktuelle design.",
"Profile / Edges": "Profil / kanter",
"Profiles or curves": "Profiler eller kurver",
"Profiles/Curves": "Profiler/kurver",
"Quantity": "Antal",
"RECENT": "SENESTE",
"REFERENCE": "REFERENCE",
"RULED SURFACE": "REGELFLADE",
"RULES": "REGLER",
"Radius": "Radius",
"Radius Fit": "Tilpasning af radius",
"Ratio": "Forhold",
"Reattach selected face": "Tilknyt den valgte flade igen",
"Recover": "Gendan",
"Recover unsaved design?": "Gendan design, der ikke er gemt?",
"Rectangular": "Rektangulær",
"Rectangular Tube": "Rektangulært hulprofil",
"Reference face": "Referenceflade",
"Remove Tools": "Fjern værktøjsemner",
"Remove condition": "Fjern betingelse",
"Remove from Folder": "Fjern fra mappe",
"Remove from list": "Fjern fra liste",
"Remove override": "Fjern tilsidesættelse",
"Remove private design details or paths before sharing.": "Fjern private designoplysninger eller filstier, før du deler.",
@@ -290,12 +396,18 @@
"Remove the column": "Fjern kolonnen",
"Remove the track": "Fjern sporet",
"Remove this point": "Fjern dette punkt",
"Rename Feature": "Omdøb feature",
"Rename Folder": "Omdøb mappe",
"Replace existing library file": "Erstat eksisterende biblioteksfil",
"Report a bug — review a screenshot and open a GitHub draft": "Rapportér en fejl — gennemgå et skærmbillede, og åbn en GitHub-kladde",
"Report text copied. Paste it into GitHub and submit your report there.": "Rapportteksten er kopieret. Indsæt den på GitHub, og indsend rapporten der.",
"Reports are public. A GitHub account is required.": "Rapporter er offentlige. Du skal have en GitHub-konto.",
"Reset Toolbar": "Nulstil værktøjslinje",
"Resize Part": "Ændr størrelse på del",
"Restore View": "Gendan visning",
"Restore outer boundary": "Gendan ydre afgrænsning",
"Restore the inherited component or physical material appearance": "Gendan udseendet fra komponenten eller det fysiske materiale",
"Restore this studio's tools": "Gendan værktøjerne i dette arbejdsområde",
"Result": "Resultat",
"Retake screenshot": "Tag skærmbillede igen",
"Reverse": "Vend om",
@@ -305,9 +417,20 @@
"Revert Pos.": "Fortryd pos.",
"Revert Position": "Tilbagefør position",
"Review the report and screenshot before opening GitHub.": "Gennemgå rapport og skærmbillede, før du åbner GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Gennemgå rapporten på GitHub, og klik på Submit new issue for at offentliggøre den.",
"Rib Length": "Ribbelængde",
"Rib Offset": "Ribbeforskydning",
"Rib Thickness": "Ribbetykkelse",
"Ribs": "Ribber",
"Right Middle": "Til højre i midten",
"Rim Edge": "Randkant",
"Ring of tools on the right button": "Værktøjsring på højre museknap",
"Roll Back Here": "Rul tilbage hertil",
"Roll History Marker Here": "Flyt tilbagerulningsmarkør hertil",
"Root radius": "Rodradius",
"Round": "Rund",
"Round Bar": "Rundstang",
"Round Tube": "Rundt hulprofil",
"Rule": "Regel",
"Rule name": "Regelnavn",
"Rules run from top to bottom. Matched drives change effective values.": "Regler køres oppefra og ned. Matchede drivere ændrer de effektive værdier.",
@@ -315,21 +438,29 @@
"SURFACE QUALITY": "OVERFLADEKVALITET",
"Save Rule": "Gem regel",
"Save View": "Gem visning",
"Save screenshot…": "Gem skærmbillede…",
"Save the study as an animated GIF, seen as the viewport shows it": "Gem undersøgelsen som en animeret GIF med samme visning som i modelvinduet",
"Saved preview. Geometry is being checked before editing.": "Gemt forhåndsvisning. Geometrien kontrolleres før redigering.",
"Scale factor": "Skaleringsfaktor",
"Scene": "Scene",
"Screenshot (optional)": "Skærmbillede (valgfrit)",
"Search": "Søg",
"Search commands": "Søg efter kommandoer",
"Search this library": "Søg i dette bibliotek",
"Second plane": "Andet plan",
"Section": "Snit",
"Section (optional)": "Tværsnit (valgfrit)",
"Section position": "Profilets placering",
"Sections (one sketch each)": "Tværsnit (én skitse til hvert)",
"Select Other": "Vælg andet",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vælg andet: Hold venstre museknap nede uden at flytte markøren på modellen (eller højreklik › Vælg andet…) for at vise alt under markøren. Klik igen samme sted for at vælge næste objekt. Dobbeltklik på den midterste museknap for at gå til Hjem (Indstillinger › Navigation).",
"Select bodies (a face picks its body)": "Vælg emner (en flade vælger sit emne)",
"Select bodies or faces instead": "Vælg emner eller flader i stedet",
"Select edges": "Vælg kanter",
"Selection filter": "Markeringsfilter",
"Semantic faces share their appearance across instances.": "Semantiske flader deler udseende på tværs af instanser.",
"Send your report on GitHub": "Send din rapport på GitHub",
"Shaded": "Skyggelagt",
"Shape": "Form",
"Sheet Rules": "Pladeregler",
"Show Agent Cursor": "Vis agentmarkør",
@@ -345,9 +476,11 @@
"Sketch plane has no body-face attachment.": "Skitseplanet er ikke tilknyttet en flade på et emne.",
"Sketch points": "Skitsepunkter",
"Slot width": "Slidsbredde",
"Snap to face and edge centres": "Snap til flade- og kantcentre",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft lukkede, før disse designs blev gemt. Den seneste automatiske lagring kan åbnes.",
"Specialise": "Specialisér",
"Speed": "Hastighed",
"Splitting tool": "Opdelingsværktøj",
"Square": "Kvadrat",
"Stand it up": "Stil op",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stil op: En kvart omdrejning om X som én flyttefeature, der kan fortrydes.",
@@ -357,17 +490,27 @@
"Start continuity": "Overgang ved starten",
"Step": "Trin",
"Steps": "Trin",
"Steps to reproduce (optional)": "Trin til at genskabe problemet (valgfrit)",
"Stitch the kept pieces": "Sammenføj de bevarede dele",
"Stitch to both bodies": "Sammenføj med begge emner",
"Studio": "Arbejdsområde",
"Study": "Undersøgelse",
"Style": "Stil",
"Summary": "Resumé",
"Suppress": "Undertryk",
"Suppressed": "Undertrykt",
"Surface": "Overflade",
"Surface, or faces to turn over": "Overflade eller flader, der skal vendes",
"Swap Ends": "Ombyt ender",
"TRIM": "TRIM",
"Tangent (G1)": "Tangentkontinuitet (G1)",
"Tangent Chain": "Tangentkæde",
"Tangent chain": "Tangentkæde",
"Tap drill diameter": "Diameter på gevindbor",
"Target": "Mål",
"Target Body": "Mållegeme",
"Tear": "Rivning",
"Technical details": "Tekniske detaljer",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Den stiplede skitse er en gemt planreference. Vælg en ny flade for at reparere tilknytningen.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Den gemte oprindelige komponentforekomst mangler. Vælg en gyldig oprindelig placering før ny tilknytning.",
"Then": "Så",
@@ -379,24 +522,36 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Dette underobjekt er delt. Sletning fjerner det fra alle kopier af dets overordnede objekt.",
"This child is shared. Position changes apply to every copy of its parent.": "Dette underobjekt er delt. Positionsændringer gælder for alle kopier af dets overordnede objekt.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Denne flade kan ikke give en varig tilknytning. Vælg en anden flade, eller vælg udtrykkeligt et fast plan.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Dette billede er endnu ikke vedhæftet på GitHub. Kopiér eller gem det, og vedhæft det derefter på GitHub.",
"To": "Til",
"To Offset": "Til forskydning",
"To edges": "Til kanter",
"Toe radius": "Tåradius",
"Tolerance": "Tolerance",
"Too close": "For tæt på",
"Tool Bodies": "Værktøjslegemer",
"Tools": "Værktøjer",
"Tools, clockwise from the top": "Værktøjer, med uret fra toppen",
"Top Left": "Øverst til venstre",
"Top Middle": "Øverst i midten",
"Top Right": "Øverst til højre",
"Trimming tool (plane, face or curves)": "Trim med plan, flade eller kurver",
"Turn off section analysis before exporting the view.": "Slå snitanalyse fra, før visningen eksporteres.",
"Type": "Type",
"UNTRIM": "FJERN BESKÆRING",
"Unit": "Enhed",
"Unsuppress": "Ophæv undertrykkelse",
"Updating the preview": "Opdaterer forhåndsvisningen",
"Use active author placement": "Brug den aktive komponents placering",
"Use fixed plane": "Brug fast plan",
"Use saved fixed plane": "Brug gemt fast plan",
"Use selected face as fixed plane": "Brug valgt flade som fast plan",
"Use the SolidWorks mouse mode": "Brug SolidWorks-musetilstanden",
"User Parameters": "Brugerparametre",
"VIEW": "VISNING",
"VIEWS": "VISNINGER",
"Value": "Værdi",
"Variable Radius": "Variabel radius",
"Velocity": "Hastighed",
"Vertices": "Hjørnepunkter",
"View navigator": "Visningsnavigator",
@@ -404,16 +559,53 @@
"Wall": "Væg",
"Wall Location": "Vægplacering",
"Wall Thickness": "Vægtykkelse",
"Web thickness": "Kroptykkelse",
"What did you do just before the problem? A short list is enough": "Hvad gjorde du lige før problemet opstod? En kort liste er nok",
"What did you expect to see?": "Hvad forventede du at se?",
"What happened?": "Hvad skete der?",
"When": "Når",
"Width": "Bredde",
"Will fail": "Vil fejle",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Når musebevægelser er slået til, roterer højre museknap ikke længere visningen: Hold den nede, og bevæg musen for at åbne ringen. Før derefter markøren ind i et værktøjs område. Et almindeligt højreklik åbner stadig menuen.",
"X": "X",
"X Angle": "X-vinkel",
"X angle": "X-vinkel",
"X distance": "X-afstand",
"Y": "Y",
"Y Angle": "Y-vinkel",
"Y angle": "Y-vinkel",
"Y distance": "Y-afstand",
"Z": "Z",
"Z Angle": "Z-vinkel",
"Z Distance": "Z-afstand",
"Z angle": "Z-vinkel",
"Z distance": "Z-afstand",
"Zebra": "Zebra",
"ai.prefs.allow_control": "Tillad AI-agenter at oprette forbindelse (kontrolkanal / MCP)",
"ai.prefs.allow_control_note": "Åbner kun en kontrolkanal på denne computer (127.0.0.1, port 27878 eller den næste ledige). Et program, der opretter forbindelse, skal også fremvise et hemmeligt token, som kun din brugerkonto kan læse, og som oprettes på ny ved hver start. Træder i kraft, næste gang SolidCraft starter. Fra: Ingen port lytter, medmindre du starter SolidCraft med --control.",
"ai.prefs.availability": "Assistentstyring",
"ai.prefs.background_commands": "Kør agentkommandoer i baggrunden",
"ai.prefs.follow_camera": "Vend kameraet mod skitser",
"ai.prefs.follow_camera_off": "Automatisk kamerafølgning er slået fra.",
"ai.prefs.follow_camera_on": "Kameraet vender sig mod skitser, som agenten opretter.",
"ai.prefs.intro": "Agenter, tilladelser og visning af deres arbejde.",
"ai.prefs.make_default": "Brug som standard",
"ai.prefs.no_agents": "Ingen agenter endnu. Tilføj en agent, eller opdatér for at finde installerede agenter.",
"ai.prefs.no_saved_approvals": "Ingen gemte godkendelser.",
"ai.prefs.override_all_warning": "Denne agent kan gemme, overskrive, eksportere eller erstatte designet uden at spørge.",
"ai.prefs.permission_all": "Tillad alt",
"ai.prefs.permission_all_summary": "Tilgængelige SolidCraft-værktøjer kører uden at spørge.",
"ai.prefs.permission_ask": "Spørg før ændringer",
"ai.prefs.permission_ask_summary": "Godkend hver modelændring og filhandling.",
"ai.prefs.permission_edits": "Tillad modelændringer",
"ai.prefs.permission_edits_summary": "Modelændringer udføres; filhandlinger og destruktive handlinger kræver stadig godkendelse.",
"ai.prefs.permission_scope": "Gælder kun SolidCraft-værktøjer. Agentens shell og filændringer bruger dens egne tilladelser.",
"ai.prefs.pointer_preview_hidden": "Markør skjult",
"ai.prefs.pointer_preview_speed": "Agentmarkør · {speed}",
"ai.prefs.pointer_speed": "Bevægelseshastighed",
"ai.prefs.privacy_note": "Kun din valgte agent eller dit valgte slutpunkt modtager samtalen. Slutpunktsnøgler gemmes i systemets nøglering.",
"ai.prefs.show_pointer": "Vis agentmarkør",
"ai.prefs.turn_limit_hint": "Det maksimale antal værktøjskald, der ændrer designet, som assistenten må foretage for én besked.",
"angle, height": "vinkel, højde",
"appearance.dielectric_ior": "Dielektrisk brydningsindeks",
"appearance.metallic": "Metallisk",
@@ -460,8 +652,10 @@
"cmd.component_activate": "Aktivér komponent",
"cmd.component_break_link": "Bryd forbindelse",
"cmd.component_capture_position": "Registrér position",
"cmd.component_copies": "Find identiske kopier",
"cmd.component_create": "Ny komponent",
"cmd.component_delete": "Slet komponent",
"cmd.component_extract": "Udtræk til fil",
"cmd.component_from_bodies": "Opret komponenter fra emner",
"cmd.component_insert_design": "Indsæt design",
"cmd.component_links": "Vis forbindelser",
@@ -474,6 +668,7 @@
"cmd.component_properties": "Komponentens fysiske egenskaber",
"cmd.component_relink": "Skift kilde",
"cmd.component_rename": "Omdøb komponent",
"cmd.component_replace_with_link": "Erstat med link",
"cmd.component_revert_position": "Tilbagefør position",
"cmd.component_unlock": "Lås indhold op",
"cmd.component_update_links": "Opdatér forbindelser",
@@ -596,8 +791,10 @@
"cmd.motion_plot": "Vis diagram for bevægelsesanalyse",
"cmd.motion_study": "Bevægelsesanalyse",
"cmd.motion_trace": "Bevægelsesspor",
"cmd.motion_trace_edit": "Redigér bevægelsesspor",
"cmd.motion_trace_keep": "Behold bevægelsesspor",
"cmd.occurrence_copy": "Indsæt (komponentforekomst)",
"cmd.occurrence_drag": "Træk forekomst",
"cmd.occurrence_ground": "Fastgør",
"cmd.occurrence_move": "Flyt komponentforekomst",
"cmd.occurrence_properties": "Komponentforekomstens fysiske egenskaber",
@@ -655,6 +852,7 @@
"cmd.sketch_auto_constrain_from_datum": "Automatisk binding ud fra reference",
"cmd.sketch_auto_project": "Projektér referencekanter automatisk",
"cmd.sketch_blend_curve": "Overgangskurve",
"cmd.sketch_cancel": "Annullér skitse",
"cmd.sketch_chain": "Kæde",
"cmd.sketch_chamfer_distance_angle": "Affasning med afstand og vinkel",
"cmd.sketch_chamfer_equal_distance": "Affasning med ens afstande",
@@ -770,6 +968,7 @@
"cmd.view_graphics_preset": "Grafikprofil",
"cmd.view_list": "Vis navngivne visninger",
"cmd.view_navigate": "Navigér i visning",
"cmd.view_normal_to": "Vinkelret på markering",
"cmd.view_rename": "Omdøb navngiven visning",
"cmd.view_save": "Ny navngiven visning",
"color.copy": "Klik for at kopiere farveværdier",
@@ -858,6 +1057,7 @@
"file_load.stopping": "Venter på, at den forrige handling stopper...",
"file_load.waiting": "Venter på filhandling",
"fits the face, e.g. M8": "passer til fladen, f.eks. M8",
"from the profile": "fra profilen",
"from the rule": "fra reglen",
"help.apache": "Apache-licens 2.0",
"help.attributions": "Tilskrivninger (ATTRIBUTION.md)",
@@ -867,6 +1067,12 @@
"help.licences": "Licenser",
"help.mit": "MIT-licens",
"help.notice": "NOTICE",
"help.solid.chamfer": "Affasning skærer de valgte kanter skråt med den angivne afstand. Brug den til at fjerne skarpe kanter eller lette indføringen af en tap eller skrue.",
"help.solid.extrude": "Ekstrudering giver en skitseprofil dybde ved at tilføje eller fjerne materiale langs en retning. Angiv afstanden for at danne en klods, en forhøjning eller en lomme. Brug Udhulning, hvis en massiv model i stedet skal gøres hul med en bestemt vægtykkelse.",
"help.solid.fillet": "Afrunding erstatter de valgte kanter med en glat, cirkulær overgang med den angivne radius. Den kan blødgøre skarpe kanter af hensyn til styrke, komfort eller udseende.",
"help.solid.hole": "Hul opretter et rundt hul ved et valgt punkt på en flade. Angiv diameteren, og vælg eventuelt konisk forsænkning, cylindrisk forsænkning eller gevind. Ved gevind vælges en gevindbetegnelse, f.eks. M6.",
"help.solid.revolve": "Omdrejning drejer en skitseprofil omkring en valgt akse og danner et omdrejningslegeme. Brug den f.eks. til aksler, knopper og skåle, der er runde omkring en centerlinje.",
"help.solid.shell": "Udhulning gør et massivt legeme hult med den angivne vægtykkelse. Vælg de flader, der skal åbnes. Indvendig udhulning bevarer de udvendige mål, mens udvendig udhulning bevarer de indvendige mål. Brug den f.eks. til kasser, dæksler og andre tyndvæggede dele.",
"help.third_party": "Tredjepartslicenser",
"home.back": "Tilbage til {name}",
"home.crashes": "Nedbrudsrapporter",
@@ -976,6 +1182,15 @@
"motion.to_end": "Til slutningen",
"motion.to_start": "Til starten",
"motion.track_name": "Spor {number}",
"nav.alias.help": "Shift+Alt+træk med venstre museknap roterer visningen, Shift+Alt+midterste panorerer, Shift+Alt+højre zoomer; træk med midterste museknap panorerer også, og træk med højre roterer visningen",
"nav.blender.help": "Træk med midterste museknap roterer visningen, Shift+midterste panorerer, Ctrl+midterste zoomer; træk med højre museknap påvirker ikke kameraet",
"nav.free_orbit.help": "Musen og 3D-musen roterer gennem begge poler og bevarer rulning. Snap til akser med Shift er slået fra.",
"nav.free_orbit.label": "Fri rotation af visningen",
"nav.fusion.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, træk med højre roterer visningen",
"nav.inventor.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, Ctrl+midterste zoomer, træk med højre roterer visningen",
"nav.openscad.help": "Træk med venstre museknap i et tomt område roterer visningen, Shift+venstre markerer med en ramme, træk i skitsegeometri redigerer den; træk med højre eller midterste museknap panorerer, Shift+højre eller Shift+midterste zoomer",
"nav.solidworks.help": "Træk med midterste museknap roterer visningen, Ctrl+midterste panorerer, Shift+midterste zoomer, træk med højre roterer visningen",
"nav.tinkercad.help": "Træk med højre museknap roterer visningen, træk med midterste eller Shift+højre panorerer",
"nav.touchpad.help": "Hold Shift nede, og bevæg markøren for at panorere; hold Alt/Option nede, og bevæg den for at rotere visningen. Ingen museknap behøver at være trykket ned. Lodret rulning zoomer.",
"nav.touchpad.label": "Pegefelt",
"nav.touchpad.orbit": "Rotér visning (hold Alt/Option nede og bevæg markøren; ingen museknap)",
@@ -998,8 +1213,130 @@
"pattern.flip_direction": "Vend retning",
"pick a hole to seat it, or OK places it at the origin": "vælg et hul til placeringen, eller tryk OK for at placere ved nulpunktet",
"pick one or two vertices, edges or faces": "vælg et eller to hjørnepunkter, kanter eller flader",
"prefs.add_item": "Tilføj elementet",
"prefs.alt_frees_manipulators": "Alt / Option frigør også manipulatorpilene",
"prefs.anti_aliasing_note": "Udjævner takkede kanter.",
"prefs.applies_on_restart": "Træder i kraft efter genstart",
"prefs.assistant": "Assistent",
"prefs.axes": "Akser",
"prefs.axis": "Akse",
"prefs.bottom": "Bund",
"prefs.browser": "Modelbrowser",
"prefs.built_in": "Indbygget",
"prefs.button_left": "Træk med venstre museknap",
"prefs.button_middle": "Træk med midterste museknap",
"prefs.button_right": "Træk med højre museknap",
"prefs.choose_font": "Vælg skrifttypefil…",
"prefs.click": "Klik",
"prefs.contact_shading": "Blød kontaktskygning",
"prefs.contact_shading_note": "Blød skygning, hvor dele er tæt på hinanden.",
"prefs.context_menu": "Genvejsmenu",
"prefs.context_menu_ring": "Genvejsmenu · hold nede for at åbne den radiale menu",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Åbn nedbrudsrapporter…",
"prefs.crash_reports_note": "Rapporter gemmes på denne computer med designet og de kommandoer, der genskaber problemet.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Krumme overflader",
"prefs.curves": "Krumme overflader",
"prefs.dead_zone_note": "Det skraverede bånd er dødzonen: Bevægelser inden for det ignoreres.",
"prefs.default": "standard",
"prefs.design": "Standardindstillinger for design",
"prefs.design_note": "Udgangspunktet for nye design. Åbne design beholder deres egne indstillinger.",
"prefs.devices": "3D-mus",
"prefs.display": "Visning",
"prefs.edges": "Kanter og skygning",
"prefs.extend": "Udvid",
"prefs.extra_navigation": "Ctrl+træk med venstre museknap i et tomt område roterer visningen; hold mellemrumstasten nede for at panorere.",
"prefs.follows_os": "Følger operativsystemet",
"prefs.font": "Skrifttype",
"prefs.font_fallback": "Tegn, som den valgte skrifttype mangler, vises med den indbyggede skrifttype, så ingen tekst viser en tom firkant.",
"prefs.font_sample": "Høj bly gom vandt fræk sexquiz · Ø12,50 mm · 45,0° · 0123456789",
"prefs.font_unreadable": "Den valgte skrifttype kunne ikke læses.",
"prefs.general": "Generelt",
"prefs.general_note": "Sprog, lagring og opdateringer.",
"prefs.gesture_orbit": "Rotér visningen",
"prefs.gesture_pan": "Panorér",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Grafik",
"prefs.graphics_note": "Afvej hastighed mod udjævning. Hvert valg viser sin virkning på et eksempel.",
"prefs.graphics_presets_note": "Forudindstillinger angiver de nedenstående valg samlet.",
"prefs.history": "Historik",
"prefs.hold_stop_snapping": "Hold nede for at slå snapping fra",
"prefs.icons_only": "Kun ikoner",
"prefs.imported_file": "Importeret fil",
"prefs.input": "Input",
"prefs.interface": "Brugergrænseflade",
"prefs.interface_language": "Sprog i brugergrænsefladen",
"prefs.interface_note": "SolidCrafts udseende og placeringen af dets dele.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Etiketter",
"prefs.layout": "Layout",
"prefs.layout_note": "Kompakt fastgør panelerne til vinduets kanter. Gennemsigtighed i modelbrowseren er tilgængelig i Flydende. Skift assistentens side på siden Assistent.",
"prefs.left": "Venstre",
"prefs.left_click": "Venstreklik",
"prefs.live_input": "Løbende input",
"prefs.live_preview": "Løbende forhåndsvisning",
"prefs.minutes": "minutter",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mus og pegefelt",
"prefs.mouse_heading": "Mus",
"prefs.mouse_mapping": "Musetildeling",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "Hvordan musen, pegefeltet og 3D-musen styrer visningen.",
"prefs.not_on_gpu": "ikke på denne GPU",
"prefs.on": "Til",
"prefs.opaque": "Uigennemsigtig",
"prefs.orbit_snap": "Hold Shift nede under rotation af visningen for at snappe til en akse.",
"prefs.orientation": "Orientering",
"prefs.overall_speed": "Samlet hastighed",
"prefs.preview_choices": "Forhåndsvisning af dine valg",
"prefs.quality_default": "standard",
"prefs.quality_fastest": "hurtigst",
"prefs.quality_slowest": "langsomst",
"prefs.quality_smoother": "mere udjævnet",
"prefs.reports_saved": "{n} gemt",
"prefs.right_click": "Højreklik",
"prefs.saving": "Automatisk lagring og gendannelse",
"prefs.scale_note": "Også Ctrl + / Ctrl − i vinduet. Knapper er 28 px ved 100%.",
"prefs.scale_text": "Skalering og tekst",
"prefs.select": "Vælg",
"prefs.select_one_item": "Vælg ét element",
"prefs.selecting": "Markering",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift tilføjer kun; Ctrl / Cmd skifter markeringen",
"prefs.shift_add_note": "Til: Shift tilføjer, Ctrl / Cmd skifter markeringen. Fra (standard): Begge skifter markeringen.",
"prefs.shortcuts": "Genveje",
"prefs.sketch_selection": "Skitse og markering",
"prefs.sketch_selection_heading": "Skitse og markering",
"prefs.sketch_selection_note": "Hvordan skitser snapper, og hvordan klik markerer.",
"prefs.sketching": "Skitsering",
"prefs.snap_drawing_note": "Mens du tegner eller trækker et skitsepunkt.",
"prefs.source": "Kilde",
"prefs.speed": "Hastighed",
"prefs.stand_up_note": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X.",
"prefs.status_done": "Færdig",
"prefs.status_error": "Fejl",
"prefs.status_selected": "Valgt",
"prefs.status_warning": "Advarsel",
"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.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nu",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Tilføj elementet, eller fjern det igen",
"prefs.toolbar": "Værktøjslinje",
"prefs.translucent": "Halvgennemsigtig",
"prefs.under_browser": "Under modelbrowseren",
"prefs.under_cursor": "Under markøren",
"prefs.units_precision": "Enheder og præcision",
"prefs.updates": "Opdateringer",
"prefs.view": "Visningens adfærd",
"prefs.viewport": "Visningsområde",
"prefs.wheel": "Musehjul",
"prefs.wheel_trackpad": "Musehjul og pegefelt",
"prefs.workspace": "Arbejdsområde",
"prefs.zoom_toward_cursor": "Zoom mod markøren",
"properties.title": "Egenskaber: {name}",
"recovery.days_ago": "for {count} dage siden",
"recovery.detail": "{file} · {features} features · autogemt {when}",
@@ -1070,16 +1407,20 @@
"search.suggest": "Mente du {label}?",
"search.try": "Skriv et kommandonavn, en feature i dette design eller en parameter. Du kører en kommando med værdier ved at skrive dens id og et JSON-objekt: {example}",
"search.valid": "Gyldig",
"settings.display_quality_note": "Finere niveauer viser krumme flader og kanter mere jævnt og tager længere tid at opbygge mesh til. Kun modelvisningen ændres, aldrig designet eller eksporten.",
"section.off_hint": "Intet plan valgt: OK slår snittet fra.",
"settings.display_quality_note": "Gælder kun visningen. Design og eksport påvirkes ikke.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen.",
"settings.measure_axis_distances": "Vis X/Y/Z-afstande",
"settings.measure_axis_distances_help": "Vis afstanden langs X-, Y- og Z-aksen sammen med den samlede afstand i Mål.",
"settings.msaa_applies_on_restart": "Kantudjævning træder i kraft, næste gang SolidCraft starter",
"settings.msaa_reset": "{setting}: {requested}× understøttes ikke af denne grafikenhed; værdien er nu {reset}× (understøttet: {supported}).",
"settings.msaa_unsupported": "Understøttes ikke af denne grafikenhed",
"settings.reference_geometry_through_bodies": "Vis referencegeometri gennem legemer",
"settings.reference_geometry_through_bodies_help": "Tegn inaktive skitsekonturer og referenceplaner oven på skyggelagte legemer",
"settings.reset_intro": "Disse indstillinger virker ikke på denne computer og ville forhindre SolidCraft i at starte. De er derfor ændret:",
"settings.reset_title": "Indstillinger blev ændret",
"settings.supersampling_note": "Tegner visningen med højere opløsning og nedskalerer den: Jævnere kanter, linjer og gitter på skærme under 4K. Det kræver GPU-tid: ca. 2,25× så mange pixels ved 1,5× og 4× ved 2×.",
"sheet.no_body": "der er intet pladelegeme: Start et med Flange > Basis, eller omdan en plade til et pladelegeme med Konvertér til plade (vælg dens store plane flade)",
"shortcut.json_error": "parametre skal være JSON: {error}",
"shortcut.no_command": "ingen kommando `{id}`; teksten før JSON skal være et kommando-id",
"shortcut.pin": "Fastgør øverst i S-boksen",
@@ -1116,6 +1457,9 @@
"thread.designation": "Betegnelse",
"thread.iso_metric": "ISO-metrisk",
"thread.select_size": "Vælg størrelse",
"timeline.delete_after_marker_one": "Slet 1 feature efter historikmarkøren",
"timeline.delete_after_marker_other": "Slet {count} features efter historikmarkøren",
"timeline.delete_many": "Slet {count} valgte features",
"timeline.group": "Ny featuremappe",
"timeline.move_many": "Flyt elementer i tidslinjen",
"timeline.move_to_group": "Flyt features til mappe",
@@ -1361,11 +1705,15 @@
"ui.agent_chat_waiting": "Venter på dig",
"ui.agent_cursor": "Agentmarkør",
"ui.agent_cursor_speed": "Agentmarkørens hastighed",
"ui.agent_off_thread": "Hold programvinduet responsivt, mens agenter udfører kommandoer",
"ui.agent_off_thread_note": "Langsomme kommandoer fra en agent (via AI-panelet eller en MCP-klient) køres i en separat tråd, så du stadig kan rotere og undersøge modellen. Du kan stoppe en kommando via statusfeltet i visningen. Når funktionen er slået fra, køres kommandoerne på programvinduets tråd, og vinduet venter, indtil de er færdige.",
"ui.ai_add": "Tilføj",
"ui.ai_add_agent": "Tilføj agent…",
"ui.ai_add_agent_title": "Tilføj agent",
"ui.ai_add_anthropic": "Anthropic-kompatibel API",
"ui.ai_add_api": "OpenAI-kompatibel API",
"ui.ai_add_custom": "Brugerdefineret kommando",
"ui.ai_add_endpoint": "API-slutpunkt",
"ui.ai_add_installed": "Installeret agent",
"ui.ai_add_installed_note": "Din egen opsætning af en installeret agent, f.eks. med andre argumenter. Installerede agenter vises automatisk.",
"ui.ai_added": "tilføjet",
@@ -1394,6 +1742,7 @@
"ui.ai_key_needs_https": "En nøgle sendes kun via HTTPS eller til denne computer.",
"ui.ai_key_note": "Gemmes i systemets nøglering, aldrig i SolidCrafts indstillinger. Lad feltet være tomt ved en lokal server.",
"ui.ai_keychain_error": "Systemets nøglering afviste anmodningen: {error}",
"ui.ai_kind": "Type",
"ui.ai_mode_all": "Tillad alt",
"ui.ai_mode_all_note": "Intet kræver godkendelse først. Nedbrudsrapporter er stadig utilgængelige, og mappebegrænsningerne gælder fortsat.",
"ui.ai_mode_all_warning": "Agenten kan gemme, overskrive, eksportere og erstatte dit design uden at spørge. Brug kun dette med en agent, du har tillid til, og på designs, du har gemt.",
@@ -1415,6 +1764,7 @@
"ui.ai_panel": "Panel",
"ui.ai_permissions": "Tilladelser",
"ui.ai_preset": "Forudindstilling",
"ui.ai_protocol": "Protokol",
"ui.ai_remove": "Fjern",
"ui.ai_reset": "Nulstil",
"ui.ai_settings": "AI-indstillinger…",
@@ -1424,6 +1774,8 @@
"ui.ai_test": "Test",
"ui.ai_test_acp_ok": "Svarer: {agent}",
"ui.ai_test_acp_tip": "Starter agenten kortvarigt og anmoder om en session; afslutter den derefter igen",
"ui.ai_test_answered": "Forbindelse oprettet: {model} svarede",
"ui.ai_test_anthropic_tip": "Sender en kort besked til slutpunktet",
"ui.ai_test_api_tip": "Henter endepunktets modeller",
"ui.ai_test_no_model": "Forbindelse oprettet: {count} modeller, men ikke {model}",
"ui.ai_test_ok": "Forbindelse oprettet: {count} modeller",
@@ -1433,8 +1785,8 @@
"ui.ai_tool_running": "kører",
"ui.ai_tool_unfold": "Klik for at vise svaret",
"ui.ai_tool_waiting": "venter",
"ui.ai_toolbar_button": "Vis knap på værktøjslinjen",
"ui.ai_toolbar_button_note": "AI-knappen i højre ende af værktøjslinjen. Ctrl+Shift+A og menuen Vis åbner stadig panelet.",
"ui.ai_toolbar_button": "Vis Assistent-knappen på programlinjen",
"ui.ai_toolbar_button_note": "Assistent-knappen på programlinjen. Du kan stadig åbne panelet med Ctrl+Shift+A eller via menuen Vis.",
"ui.ai_undo_tip": "Fortryd alt, agenten ændrede for denne besked, som ét trin",
"ui.ai_url_hint": "https://example.com/v1",
"ui.all": "Alle",
@@ -1582,7 +1934,12 @@
"ui.export_keymap_tip": "Gem tastaturgenveje, favoritter i S-menuen og værktøjslinjen i en fil",
"ui.extend": "Forlæng",
"ui.extent_type": "Udstrækningstype",
"ui.extract_component": "Udtræk til fil…",
"ui.extract_design_changed": "Designet blev ændret, mens du valgte en fil. Start Udtræk til fil igen.",
"ui.extract_done": "Komponenten er nu et selvstændigt design, der er linket hertil.",
"ui.extrude": "Ekstrudering",
"ui.extrude_auto_to_object": "Ekstrudér til et valgt mål",
"ui.extrude_auto_to_object_tip": "Når en profil er valgt i Ekstrudering, skifter udstrækningen til Til objekt, hvis du vælger en flade, et hjørnepunkt eller et parallelt plan uden for profilens plan",
"ui.factor": "Faktor",
"ui.feature_history": "Featuretidslinje",
"ui.feature_options": "Featureindstillinger",
@@ -1592,6 +1949,8 @@
"ui.filter_commands_or_keys": "Filtrér kommandoer eller taster",
"ui.findInTimeline": "Find i tidslinje",
"ui.findInWindow": "Find i vindue",
"ui.fine": "Fin",
"ui.finest": "Fineste",
"ui.finish_sketch": "Afslut skitse",
"ui.fit": "Tilpas",
"ui.fix_unfix": "Lås fast/frigør",
@@ -1689,10 +2048,17 @@
"ui.hideProfile": "Skjul profil",
"ui.history_back": "Gå ét trin tilbage",
"ui.history_below_browser": "Under browseren",
"ui.history_edge_hint": "dobbeltklik på kanten",
"ui.history_first": "Gå til begyndelsen",
"ui.history_forward": "Gå ét trin frem",
"ui.history_hide_names": "Skjul navne",
"ui.history_last": "Gå til slutningen",
"ui.history_marker_tip": "Historikmarkør: Træk for at rulle modellen tilbage; nye features indsættes her",
"ui.history_names": "Navne",
"ui.history_play": "Afspil til slutningen",
"ui.history_show_names": "Vis navne",
"ui.history_title": "Historik",
"ui.history_toggle_names": "Vis eller skjul featurenavne",
"ui.hole": "Hul",
"ui.hole_tap_type": "Hullets gevindtype",
"ui.hole_type": "Hultype",
@@ -1714,6 +2080,10 @@
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
"ui.link_copies": "Link identiske kopier",
"ui.link_copies_keep": "Behold",
"ui.link_copies_link": "Link",
"ui.link_copies_text": "Disse komponenter danner de samme legemer som {name}. Skal de gøres til placeringer af dens link?",
"ui.lock": "Lås",
"ui.loft": "Loft",
"ui.look_at": "Se på",
@@ -1816,6 +2186,7 @@
"ui.project_face_edges_sketch": "Projektér fladekanter til nye skitser",
"ui.properties": "Egenskaber",
"ui.pull_direction": "Udtræksretning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Afslut",
@@ -1827,6 +2198,9 @@
"ui.rectangle": "Rektangel",
"ui.rectangular_pattern": "Rektangulært mønster",
"ui.redo": "Gentag",
"ui.reference_1": "Reference 1",
"ui.reference_2": "Reference 2",
"ui.reference_distance": "Referenceafstand",
"ui.relink_design_changed": "Samlingen blev ændret, mens du valgte en kilde. Start Skift kilde igen.",
"ui.relink_done": "Kilden er ændret for alle forekomster.",
"ui.relink_not_linked": "Denne komponent er ikke knyttet til et kildedesign.",
@@ -1837,6 +2211,7 @@
"ui.render": "Rendering",
"ui.renderer": "Renderingsmotor",
"ui.repeat": "Gentag",
"ui.replace_with_link_done": "Kopierne er nu placeringer af det linkede design.",
"ui.report_a_bug": "Rapportér en fejl",
"ui.reset": "Nulstil",
"ui.reset_all": "Nulstil alle",
@@ -1875,6 +2250,12 @@
"ui.sent_to_slicer": "Sendt til {slicer}",
"ui.sheet_gap": "Mellemrum",
"ui.shell": "Udhulning",
"ui.shortcut_capture_add_key": "Tilføj én almindelig tast. Escape annullerer.",
"ui.shortcut_capture_hint": "Klik på en tastetildeling, og tryk derefter på modifikationstaster og én tast. Tilføj modifikationstaster inden for ét sekund; slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_press": "Tryk på modifikationstaster og én tast. Escape annullerer.",
"ui.shortcut_capture_release": "Slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_settling": "Bekræfter genvej… Escape annullerer.",
"ui.shortcut_multiple_keys": "Brug modifikationstaster og kun én anden tast. Klik på tastetildelingen for at prøve igen.",
"ui.show": "Vis",
"ui.showAll": "Vis alle",
"ui.showAllBodies": "Vis alle emner",
@@ -1910,6 +2291,7 @@
"ui.spline": "Spline",
"ui.split_body": "Opdel emne",
"ui.stand_up_tip": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Start forfra",
"ui.start_page": "Startside",
@@ -1937,7 +2319,7 @@
"ui.tie_ends": "Sammenbind",
"ui.tie_ends_tip": "Bind alle ubundne ender",
"ui.timeline_bottom": "Tidslinje nederst",
"ui.timeline_left_note": "Layoutet til venstre viser tilbagerulningslinjen over modeltræet med emner og scene.",
"ui.timeline_left_note": "Den lodrette historik vises til venstre for modelbrowseren. Featurenavne kan vises eller skjules på strimlen.",
"ui.timeline_left_sidebar": "Venstre sidepanel",
"ui.tip_angle": "Spidsvinkel",
"ui.to_object": "Til objekt",
@@ -1945,7 +2327,7 @@
"ui.top": "Ovenfra",
"ui.torus": "Torus",
"ui.total_angle": "Samlet vinkel",
"ui.trackpad_pan_tip": "Bevægelser på pegefeltet panorerer (slået fra: zoomer som musehjulet); musehjulet zoomer, tommelfingerhjulet panorerer sidelæns, og knibebevægelse zoomer altid",
"ui.trackpad_pan_tip": "Rulning med to fingre panorerer; i Fusion roterer Shift + to fingre visningen. Fra: Rulning zoomer. Musehjul og knibebevægelser zoomer; tommelfingerhjulet panorerer sidelæns.",
"ui.trim": "Trim",
"ui.twist": "Vridning",
"ui.two_finger_scroll_pans": "Rulning med to fingre panorerer",
@@ -2022,5 +2404,6 @@
"update.up_to_date": "SolidCraft er opdateret.",
"update.update_now": "Opdatér nu",
"update.you_have": "Du har {version}.",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk"
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ vælg · Enter anvender det indtastede · Esc lukker"
}
+5
View File
@@ -1847,6 +1847,8 @@
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
"ui.findInTimeline": "In der Zeitleiste suchen",
"ui.findInWindow": "Im Fenster suchen",
"ui.fine": "Fein",
"ui.finest": "Sehr fein",
"ui.finish_sketch": "Skizze beenden",
"ui.fit": "Einpassen",
"ui.fix_unfix": "Fixieren/Lösen",
@@ -2075,6 +2077,7 @@
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
"ui.properties": "Eigenschaften",
"ui.pull_direction": "Entformungsrichtung",
"ui.quality": "Qualität",
"ui.quantity": "Anzahl",
"ui.quantity_2": "Anzahl 2",
"ui.quit": "Beenden",
@@ -2176,6 +2179,7 @@
"ui.spline": "Spline",
"ui.split_body": "Körper trennen",
"ui.stand_up_tip": "Nach dem Öffnen einer STEP- oder IGES-Datei mit Z nach oben eine Vierteldrehung um X anbieten",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Von vorn beginnen",
"ui.start_page": "Startseite",
@@ -2278,6 +2282,7 @@
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
"update.prefs_label": "Updates",
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
"update.prefs_web": "Laden Sie die Seite neu, um die neueste Web-App zu erhalten. Ist das Netzwerk nicht verfügbar oder kann das Update nicht rechtzeitig vollständig heruntergeladen werden, wird die gespeicherte Version geöffnet. Ein laufender Download wird für das nächste Neuladen fortgesetzt.",
"update.release_page": "Release-Seite öffnen",
"update.restart": "Jetzt neu starten",
"update.restart_later": "Später",
+46 -2
View File
@@ -143,6 +143,7 @@
"Cube": "Cube",
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
"Current perspective view": "Current perspective view",
"Current plane": "Current plane",
"Curv. Comb": "Curv. Comb",
"Curvature (G2)": "Curvature (G2)",
"Curvature change (G3)": "Curvature change (G3)",
@@ -239,7 +240,9 @@
"Gap": "Gap",
"Grid": "Grid",
"HISTORY": "HISTORY",
"Halo under sketch curves": "Halo under sketch curves",
"Height": "Height",
"Height From": "Height From",
"Hidden edges": "Hidden edges",
"History layout": "History layout",
"History names": "History names",
@@ -256,6 +259,7 @@
"In place (surfaces)": "In place (surfaces)",
"In plane": "In plane",
"Included": "Included",
"Inner Sharp": "Inner Sharp",
"Inside": "Inside",
"Instant": "Instant",
"Interface font": "Interface font",
@@ -302,6 +306,7 @@
"Mass Center": "Mass Center",
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
"Material": "Material",
"Material Outside": "Material Outside",
"Material copied. Existing body assignments are unchanged.": "Material copied. Existing body assignments are unchanged.",
"Max": "Max",
"Maximum": "Maximum",
@@ -314,7 +319,6 @@
"Mitre": "Mitre",
"Model Parameters": "Model Parameters",
"Modeled": "Modeled",
"More themes": "More themes",
"More toolbar panels": "More toolbar panels",
"Motion": "Motion",
"Mouse gestures": "Mouse gestures",
@@ -349,8 +353,10 @@
"On your computer": "On your computer",
"Opacity": "Opacity",
"Open GitHub draft": "Open GitHub draft",
"Open bodies (no volume)": "Open bodies (no volume)",
"Open library...": "Open library...",
"Open local libraries in the desktop app. You can export this design's library here.": "Open local libraries in the desktop app. You can export this design's library here.",
"Open surface — no volume": "Open surface — no volume",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Opens your browser and copies the report text for you to paste into GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Opens your browser. Review and submit your report on GitHub.",
@@ -359,6 +365,7 @@
"Other side": "Other side",
"Other surface (mutual trim)": "Other surface (mutual trim)",
"Other way": "Other way",
"Outer Sharp": "Outer Sharp",
"Outer radius": "Outer radius",
"Outside": "Outside",
"Overall length": "Overall length",
@@ -388,7 +395,6 @@
"Preview": "Preview",
"Preview Row": "Preview Row",
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
"Previous themes": "Previous themes",
"Profile / Edges": "Profile / Edges",
"Profiles or curves": "Profiles or curves",
"Profiles/Curves": "Profiles/Curves",
@@ -526,9 +532,11 @@
"Surface, or faces to turn over": "Surface, or faces to turn over",
"Swap Ends": "Swap Ends",
"TRIM": "TRIM",
"Tangent": "Tangent",
"Tangent (G1)": "Tangent (G1)",
"Tangent Chain": "Tangent Chain",
"Tangent chain": "Tangent chain",
"Tangent to Edge": "Tangent to Edge",
"Tap drill diameter": "Tap drill diameter",
"Target": "Target",
"Target Body": "Target Body",
@@ -1081,6 +1089,7 @@
"crash.report": "Report…",
"crash.unclean_explanation": "Detected on a later launch; the exact exit time was not recorded. A system shutdown or forced stop can also cause an unclean exit.",
"delete.and_more": "… and {count} more",
"diagnostic.empty_boolean_component": "there is no body to cut or intersect in component '{component}'; the tool may overlap '{body}' in component '{other}' — activate that component or pick the body as a target",
"dialog.bodies_count": "{count} bodies",
"dialog.clearance_invalid": "the clearance must be a length of 0 or more",
"dialog.compute": "Compute",
@@ -1127,6 +1136,7 @@
"file_load.built": "Built so far: {built} of {total} features.",
"file_load.converting": "Converting solid {done} of {total}",
"file_load.inserting": "Inserting geometry",
"file_load.link_item": "{name} ({done} of {total})",
"file_load.opening": "Opening design",
"file_load.preparing_thumbnail": "Preparing the thumbnail...",
"file_load.preparing_view": "Preparing the view...",
@@ -1136,6 +1146,7 @@
"file_load.rebuilding": "Rebuilding feature {done} of {total}",
"file_load.recovering": "Recovering design",
"file_load.stopping": "Waiting for the previous operation to stop...",
"file_load.updating_links": "Updating links",
"file_load.waiting": "Waiting for file operation",
"fits the face, e.g. M8": "fits the face, e.g. M8",
"from the profile": "from the profile",
@@ -1238,6 +1249,8 @@
"joint.from_bodies_hint": "Each picked body moves into a new component of its own and stays where it is.",
"joint.one_body": "Both picks are on {body}. Pick the second one on another part.",
"joint.one_component": "These parts are bodies of one component: {bodies}. A joint moves one component against another, so each part needs a component of its own.",
"joint.rigid_group_one_component": "These {count} bodies are all in one component (or in the design itself): use {command} first.",
"joint.rigid_group_select_components": "Select two or more components.",
"keymap.command_id": "Command ID",
"keymap.empty": "No matching commands",
"keymap.use_anyway": "Use {key} here anyway",
@@ -1401,6 +1414,10 @@
"prefs.gesture_pan": "Pan",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Graphics",
"prefs.graphics_card": "Graphics card",
"prefs.graphics_card_auto": "Automatic",
"prefs.graphics_card_missing": "The saved graphics card is unavailable; Automatic is used.",
"prefs.graphics_card_restart": "Takes effect after restarting SolidCraft. WGPU_ADAPTER_NAME overrides this choice.",
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note": "Presets set the choices below together.",
"prefs.history": "History",
@@ -1432,6 +1449,7 @@
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
"prefs.orientation": "Orientation",
"prefs.overall_speed": "Overall speed",
"prefs.prefer_free_sketch_points": "Prefer movable sketch points when points overlap",
"prefs.preview_choices": "Preview of your choices",
"prefs.quality_default": "default",
"prefs.quality_fastest": "fastest",
@@ -2138,6 +2156,11 @@
"sketch_panel.author": "Author placement: {path}",
"sketch_panel.dimension_hint": "Enter a value or expression, then press OK.",
"sketch_panel.move_hint": "Enter distances and an angle, then press OK.",
"sketch_panel.plane_angle": "{plane}, turned by {angle}",
"sketch_panel.plane_fixed": "A fixed plane",
"sketch_panel.plane_flipped": "{plane}, from its other side",
"sketch_panel.plane_offset": "{plane}, offset by {distance}",
"sketch_panel.plane_origin": "The {plane} plane",
"sketch_panel.redefine_title": "{command}: {name}",
"sketch_panel.sheet_attachment": "Attached to a flat face of a sheet metal body; it follows that panel when the sheet changes.",
"sketch_panel.text_hint": "Edit the text and its placement, then press OK.",
@@ -2312,6 +2335,7 @@
"ui.agent_chat_changes": "Changes: {n}, one undo step",
"ui.agent_chat_changes_n": "{n} changes",
"ui.agent_chat_changes_of": "{n}/{limit} changes",
"ui.agent_chat_choose_model": "Choose a model",
"ui.agent_chat_close": "Close",
"ui.agent_chat_command": "Agent command",
"ui.agent_chat_connect": "Connect",
@@ -2387,6 +2411,9 @@
"ui.agent_chat_previous_picture": "Previous picture",
"ui.agent_chat_privacy": "Nothing goes anywhere except to the agent you run, under your own account.",
"ui.agent_chat_privacy_note": "SolidCraft sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_provider_models": "{count} models",
"ui.agent_chat_provider_other": "Other",
"ui.agent_chat_provider_tip": "The provider whose models the next list shows.",
"ui.agent_chat_queue": "Queue",
"ui.agent_chat_queue_full": "Too many messages are waiting: remove one or wait for the agent.",
"ui.agent_chat_queued": "Queued",
@@ -2594,6 +2621,8 @@
"ui.copy": "Copy",
"ui.corner": "Corner",
"ui.corners": "Corners",
"ui.cosmetic_thread_markings": "Cosmetic thread markings",
"ui.cosmetic_thread_markings_help": "Show schematic markings on threaded cylindrical walls. Changes only the display, never the design or exports.",
"ui.counterbore": "Counterbore",
"ui.countersink": "Countersink",
"ui.create": "Create",
@@ -2670,6 +2699,8 @@
"ui.filter_commands_or_keys": "Filter commands or keys",
"ui.findInTimeline": "Find in Timeline",
"ui.findInWindow": "Find in Window",
"ui.fine": "Fine",
"ui.finest": "Finest",
"ui.finish_sketch": "Finish Sketch",
"ui.fit": "Fit",
"ui.fix_unfix": "Fix/Unfix",
@@ -2882,6 +2913,11 @@
"ui.paste": "Paste",
"ui.path": "Path",
"ui.pattern_on_path": "Pattern on Path",
"ui.perforate_margin_default": "from the diameter",
"ui.perforate_pitch": "Pitch",
"ui.perforate_square": "Square",
"ui.perforate_staggered": "Staggered (60°)",
"ui.perforate_title": "PERFORATE",
"ui.perpendicular": "Perpendicular",
"ui.perspective_view": "Perspective view",
"ui.pinToShortcuts": "Pin to Shortcuts",
@@ -2893,7 +2929,9 @@
"ui.plugins": "Plugins",
"ui.plugins_desktop": "Plugins are available in the desktop app.",
"ui.plugins_empty": "No plugins installed.",
"ui.plugins_none": "No plugins match this search.",
"ui.plugins_note": "Each plugin is a folder inside {path} with a plugin.json. SolidCraft reads that file and does not run any code from the folder. Turning a plugin off keeps it installed.",
"ui.plugins_search": "Search plugins",
"ui.plugins_skipped": "{count} folder(s) could not be read.",
"ui.plugins_version": "{version}",
"ui.point": "Point",
@@ -2909,6 +2947,7 @@
"ui.project_face_edges_sketch": "Project face edges into new sketches",
"ui.properties": "Properties",
"ui.pull_direction": "Pull direction",
"ui.quality": "Quality",
"ui.quantity": "Quantity",
"ui.quantity_2": "Quantity 2",
"ui.quit": "Quit",
@@ -3015,6 +3054,7 @@
"ui.spline": "Spline",
"ui.split_body": "Split Body",
"ui.stand_up_tip": "After a STEP or IGES file opens with Z up, offer a quarter turn about X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Start over",
"ui.start_page": "Start page",
@@ -3124,6 +3164,7 @@
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software center. SolidCraft doesn't check.",
"update.prefs_label": "Updates",
"update.prefs_note": "This is SolidCraft {version} ({kind}). A check asks GitHub for the latest release and sends nothing else.",
"update.prefs_web": "Reload the page to get the latest web app. If the network is unavailable or the update cannot finish downloading in time, the saved version opens instead; a download in progress continues for the next reload.",
"update.release_page": "Open release page",
"update.restart": "Restart now",
"update.restart_later": "Later",
@@ -3134,6 +3175,9 @@
"update.up_to_date": "SolidCraft is up to date.",
"update.update_now": "Update now",
"update.you_have": "You have {version}.",
"web.open_full": "Open in SolidCraft",
"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"
}
+4
View File
@@ -2240,6 +2240,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos o teclas",
"ui.findInTimeline": "Buscar en la línea de tiempo",
"ui.findInWindow": "Buscar en la ventana",
"ui.fine": "Fina",
"ui.finest": "Muy fina",
"ui.finish_sketch": "Finalizar boceto",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fijar/liberar",
@@ -2464,6 +2466,7 @@
"ui.project_face_edges_sketch": "Proyectar aristas de caras en bocetos nuevos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de tracción",
"ui.quality": "Calidad",
"ui.quantity": "Cantidad",
"ui.quantity_2": "Cantidad 2",
"ui.quit": "Salir",
@@ -2558,6 +2561,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir cuerpo",
"ui.stand_up_tip": "Tras abrir un archivo STEP o IGES con Z hacia arriba, ofrecer un cuarto de vuelta alrededor de X",
"ui.standard": "Estándar",
"ui.start": "Iniciar",
"ui.start_over": "Volver a empezar",
"ui.start_page": "Página de inicio",
+4
View File
@@ -1606,6 +1606,8 @@
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
"ui.findInTimeline": "Trouver dans la chronologie",
"ui.findInWindow": "Trouver dans la fenêtre",
"ui.fine": "Fine",
"ui.finest": "Très fine",
"ui.finish_sketch": "Terminer l'esquisse",
"ui.fit": "Cadrer",
"ui.fix_unfix": "Fixer/Libérer",
@@ -1830,6 +1832,7 @@
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
"ui.properties": "Propriétés",
"ui.pull_direction": "Direction de démoulage",
"ui.quality": "Qualité",
"ui.quantity": "Quantité",
"ui.quantity_2": "Quantité 2",
"ui.quit": "Quitter",
@@ -1924,6 +1927,7 @@
"ui.spline": "Spline",
"ui.split_body": "Scinder le corps",
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
"ui.standard": "Standard",
"ui.start": "Début",
"ui.start_over": "Recommencer",
"ui.start_page": "Page d’accueil",
+4
View File
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
"ui.findInTimeline": "Buscar na liña de tempo",
"ui.findInWindow": "Buscar na xanela",
"ui.fine": "Fina",
"ui.finest": "Moi fina",
"ui.finish_sketch": "Rematar o bosquexo",
"ui.fit": "Axustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de extracción",
"ui.quality": "Calidade",
"ui.quantity": "Cantidade",
"ui.quantity_2": "Cantidade 2",
"ui.quit": "Saír",
@@ -1910,6 +1913,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir o corpo",
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
"ui.standard": "Estándar",
"ui.start": "Inicio",
"ui.start_over": "Volver comezar",
"ui.start_page": "Páxina de inicio",
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
"ui.findInTimeline": "Keresés az idővonalon",
"ui.findInWindow": "Keresés az ablakban",
"ui.fine": "Finom",
"ui.finest": "Legfinomabb",
"ui.finish_sketch": "Vázlat befejezése",
"ui.fit": "Illesztés",
"ui.fix_unfix": "Rögzítés/feloldás",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
"ui.properties": "Tulajdonságok",
"ui.pull_direction": "Húzási irány",
"ui.quality": "Minőség",
"ui.quantity": "Mennyiség",
"ui.quantity_2": "2. mennyiség",
"ui.quit": "Kilépés",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Test felosztása",
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
"ui.standard": "Normál",
"ui.start": "Kezdés",
"ui.start_over": "Újrakezdés",
"ui.start_page": "Kezdőoldal",
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
"ui.findInTimeline": "Cari di Linimasa",
"ui.findInWindow": "Cari di Jendela",
"ui.fine": "Halus",
"ui.finest": "Sangat halus",
"ui.finish_sketch": "Selesai Sketsa",
"ui.fit": "Pas",
"ui.fix_unfix": "Kunci/Lepas Kunci",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
"ui.properties": "Properti",
"ui.pull_direction": "Arah tarik",
"ui.quality": "Kualitas",
"ui.quantity": "Jumlah",
"ui.quantity_2": "Jumlah 2",
"ui.quit": "Keluar",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Belah Bodi",
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
"ui.standard": "Standar",
"ui.start": "Mulai",
"ui.start_over": "Mulai ulang",
"ui.start_page": "Halaman awal",
+4
View File
@@ -1608,6 +1608,8 @@
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
"ui.findInTimeline": "Trova nella sequenza temporale",
"ui.findInWindow": "Trova nella finestra",
"ui.fine": "Fine",
"ui.finest": "Finissima",
"ui.finish_sketch": "Termina schizzo",
"ui.fit": "Adatta",
"ui.fix_unfix": "Fissa/Libera",
@@ -1833,6 +1835,7 @@
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
"ui.properties": "Proprietà",
"ui.pull_direction": "Direzione di estrazione",
"ui.quality": "Qualità",
"ui.quantity": "Quantità",
"ui.quantity_2": "Quantità 2",
"ui.quit": "Esci",
@@ -1927,6 +1930,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividi corpo",
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
"ui.standard": "Standard",
"ui.start": "Inizio",
"ui.start_over": "Ricomincia",
"ui.start_page": "Pagina iniziale",
+46 -2
View File
@@ -143,6 +143,7 @@
"Cube": "キューブ",
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
"Current perspective view": "現在の透視投影",
"Current plane": "現在の平面",
"Curv. Comb": "曲率コーム",
"Curvature (G2)": "曲率 (G2)",
"Curvature change (G3)": "曲率変化 (G3)",
@@ -239,7 +240,9 @@
"Gap": "間隔",
"Grid": "グリッド",
"HISTORY": "履歴",
"Halo under sketch curves": "スケッチ曲線の下に縁取りを表示",
"Height": "高さ",
"Height From": "高さの基準",
"Hidden edges": "隠線",
"History layout": "履歴のレイアウト",
"History names": "履歴名",
@@ -256,6 +259,7 @@
"In place (surfaces)": "その場で(サーフェス)",
"In plane": "平面内",
"Included": "挟角",
"Inner Sharp": "内側仮想シャープ",
"Inside": "内側",
"Instant": "即時",
"Interface font": "インターフェースフォント",
@@ -302,6 +306,7 @@
"Mass Center": "重心",
"Matched restrictions check the final values.": "条件に一致する制限ルールが最終値を確認します。",
"Material": "材料",
"Material Outside": "材料外側",
"Material copied. Existing body assignments are unchanged.": "マテリアルをコピーしました。既存のボディの割り当ては変わりません。",
"Max": "最大",
"Maximum": "最大",
@@ -314,7 +319,6 @@
"Mitre": "留め継ぎ",
"Model Parameters": "モデルパラメータ",
"Modeled": "モデル化済み",
"More themes": "その他のテーマ",
"More toolbar panels": "その他のツールバーパネル",
"Motion": "モーション",
"Mouse gestures": "マウス ジェスチャ",
@@ -349,8 +353,10 @@
"On your computer": "このコンピューター上",
"Opacity": "不透明度",
"Open GitHub draft": "GitHubの下書きを開く",
"Open bodies (no volume)": "開いたボディ(体積なし)",
"Open library...": "ライブラリを開く...",
"Open local libraries in the desktop app. You can export this design's library here.": "ローカルライブラリはデスクトップアプリで開いてください。このデザインのライブラリはここで書き出せます。",
"Open surface — no volume": "開いたサーフェス — 体積なし",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
"Opens your browser and copies the report text for you to paste into GitHub.": "ブラウザーを開き、GitHubに貼り付ける報告本文をコピーします。",
"Opens your browser. Review and submit your report on GitHub.": "ブラウザーを開きます。GitHubで内容を確認して投稿してください。",
@@ -359,6 +365,7 @@
"Other side": "反対側",
"Other surface (mutual trim)": "相手のサーフェス(相互トリム)",
"Other way": "反対方向",
"Outer Sharp": "外側仮想シャープ",
"Outer radius": "外側コーナー半径",
"Outside": "外側",
"Overall length": "全長",
@@ -388,7 +395,6 @@
"Preview": "プレビュー",
"Preview Row": "行をプレビュー",
"Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。",
"Previous themes": "前のテーマ",
"Profile / Edges": "プロファイル / エッジ",
"Profiles or curves": "プロファイルまたは曲線",
"Profiles/Curves": "プロファイル/曲線",
@@ -526,9 +532,11 @@
"Surface, or faces to turn over": "サーフェス、または裏返す面",
"Swap Ends": "端点を入れ替え",
"TRIM": "トリム",
"Tangent": "正接",
"Tangent (G1)": "接線 (G1)",
"Tangent Chain": "正接チェーン",
"Tangent chain": "接線チェーン",
"Tangent to Edge": "エッジに正接",
"Tap drill diameter": "タップ下穴径",
"Target": "ターゲット",
"Target Body": "ターゲットボディ",
@@ -1081,6 +1089,7 @@
"crash.report": "報告…",
"crash.unclean_explanation": "次回以降の起動時に検出されました。正確な終了時刻は記録されていません。システムのシャットダウンや強制停止によっても、正常終了が記録されない場合があります。",
"delete.and_more": "…ほか{count}件",
"diagnostic.empty_boolean_component": "コンポーネント '{component}' に切り取りまたは交差の対象となるボディがありません。ツールはコンポーネント '{other}' の '{body}' と重なる可能性があります。そのコンポーネントをアクティブにするか、ボディを対象として選択してください",
"dialog.bodies_count": "{count} ボディ",
"dialog.clearance_invalid": "クリアランスは 0 以上の長さにしてください",
"dialog.compute": "計算",
@@ -1127,6 +1136,7 @@
"file_load.built": "構築済み: 全{total}フィーチャー中{built}件。",
"file_load.converting": "ソリッドを変換中: {done}/{total}",
"file_load.inserting": "ジオメトリを挿入中",
"file_load.link_item": "{name} ({done}/{total})",
"file_load.opening": "デザインを開いています",
"file_load.preparing_thumbnail": "サムネイルを準備中...",
"file_load.preparing_view": "ビューを準備中...",
@@ -1136,6 +1146,7 @@
"file_load.rebuilding": "フィーチャーを再構築中: {done}/{total}",
"file_load.recovering": "デザインを復元中",
"file_load.stopping": "前の操作の停止を待っています...",
"file_load.updating_links": "リンクを更新中",
"file_load.waiting": "ファイル操作を待っています",
"fits the face, e.g. M8": "面に適合するサイズ (例: M8)",
"from the profile": "形材から",
@@ -1238,6 +1249,8 @@
"joint.from_bodies_hint": "選択した各ボディを、位置はそのままで専用の新しいコンポーネントに移動します。",
"joint.one_body": "両方の選択が {body} 上にあります。2 つ目は別の部品で選択してください。",
"joint.one_component": "これらの部品は 1 つのコンポーネントのボディです: {bodies}。ジョイントはコンポーネント同士を動かすため、部品ごとに専用のコンポーネントが必要です。",
"joint.rigid_group_one_component": "これらの {count} 個のボディは、すべて 1 つのコンポーネント内(またはデザイン自体)にあります。先に「{command}」を使用してください。",
"joint.rigid_group_select_components": "2 つ以上のコンポーネントを選択してください。",
"keymap.command_id": "コマンドID",
"keymap.empty": "一致するコマンドはありません",
"keymap.use_anyway": "ここでは{key}をそのまま使用",
@@ -1401,6 +1414,10 @@
"prefs.gesture_pan": "移動",
"prefs.gesture_zoom": "ズーム",
"prefs.graphics": "グラフィックス",
"prefs.graphics_card": "グラフィックスカード",
"prefs.graphics_card_auto": "自動",
"prefs.graphics_card_missing": "保存されたグラフィックスカードが利用できないため、自動選択を使用します。",
"prefs.graphics_card_restart": "SolidCraft の再起動後に有効になります。WGPU_ADAPTER_NAME がこの選択より優先されます。",
"prefs.graphics_note": "速度と滑らかさのバランスを調整します。各選択肢の効果をサンプルで確認できます。",
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
"prefs.history": "履歴",
@@ -1432,6 +1449,7 @@
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
"prefs.orientation": "方向",
"prefs.overall_speed": "全体の速度",
"prefs.prefer_free_sketch_points": "スケッチ点が重なったときは移動可能な点を優先する",
"prefs.preview_choices": "選択した設定のプレビュー",
"prefs.quality_default": "既定",
"prefs.quality_fastest": "最速",
@@ -2138,6 +2156,11 @@
"sketch_panel.author": "作成時の配置: {path}",
"sketch_panel.dimension_hint": "値または式を入力し、OK を押してください。",
"sketch_panel.move_hint": "距離と角度を入力し、OK を押してください。",
"sketch_panel.plane_angle": "{plane}、{angle} 回転",
"sketch_panel.plane_fixed": "固定平面",
"sketch_panel.plane_flipped": "{plane}(裏側から)",
"sketch_panel.plane_offset": "{plane}、{distance} オフセット",
"sketch_panel.plane_origin": "{plane} 平面",
"sketch_panel.redefine_title": "{command}:{name}",
"sketch_panel.sheet_attachment": "板金ボディの平面に接続されています。板金が変更されると、そのパネルに追従します。",
"sketch_panel.text_hint": "テキストと配置を編集し、OK を押してください。",
@@ -2312,6 +2335,7 @@
"ui.agent_chat_changes": "{n} 件の変更(元に戻すは1回)",
"ui.agent_chat_changes_n": "{n} 件の変更",
"ui.agent_chat_changes_of": "変更 {n}/{limit}",
"ui.agent_chat_choose_model": "モデルを選択",
"ui.agent_chat_close": "閉じる",
"ui.agent_chat_command": "エージェントのコマンド",
"ui.agent_chat_connect": "接続",
@@ -2387,6 +2411,9 @@
"ui.agent_chat_previous_picture": "前の画像",
"ui.agent_chat_privacy": "あなたのアカウントで実行するエージェント以外には、何も送信されません。",
"ui.agent_chat_privacy_note": "SolidCraft はどこにも何も送信しません。あなたが実行するエージェントが、あなた自身のアカウントでそのサービスと通信します。SolidCraft がキーやパスワードを見たり保存したりすることはありません。",
"ui.agent_chat_provider_models": "{count} 個のモデル",
"ui.agent_chat_provider_other": "その他",
"ui.agent_chat_provider_tip": "隣の一覧にモデルを表示するプロバイダー。",
"ui.agent_chat_queue": "待機に追加",
"ui.agent_chat_queue_full": "待機中のメッセージが多すぎます。削除するか、エージェントの完了をお待ちください。",
"ui.agent_chat_queued": "待機中",
@@ -2594,6 +2621,8 @@
"ui.copy": "コピー",
"ui.corner": "コーナー",
"ui.corners": "コーナー",
"ui.cosmetic_thread_markings": "ねじの簡略表示",
"ui.cosmetic_thread_markings_help": "ねじ付き円筒面に簡略化したマークを表示します。表示のみを変更し、設計やエクスポートには影響しません。",
"ui.counterbore": "座ぐり",
"ui.countersink": "皿穴",
"ui.create": "作成",
@@ -2670,6 +2699,8 @@
"ui.filter_commands_or_keys": "コマンドまたはキーを検索",
"ui.findInTimeline": "タイムラインで検索",
"ui.findInWindow": "ウィンドウで検索",
"ui.fine": "細かい",
"ui.finest": "最も細かい",
"ui.finish_sketch": "スケッチを終了",
"ui.fit": "フィット",
"ui.fix_unfix": "固定/固定解除",
@@ -2882,6 +2913,11 @@
"ui.paste": "貼り付け",
"ui.path": "パス",
"ui.pattern_on_path": "パス上のパターン",
"ui.perforate_margin_default": "直径と同じ",
"ui.perforate_pitch": "ピッチ",
"ui.perforate_square": "正方配列",
"ui.perforate_staggered": "千鳥配列(60°)",
"ui.perforate_title": "パーフォレーション",
"ui.perpendicular": "直交",
"ui.perspective_view": "パースペクティブ表示",
"ui.pinToShortcuts": "ショートカットに固定",
@@ -2893,7 +2929,9 @@
"ui.plugins": "プラグイン",
"ui.plugins_desktop": "プラグインはデスクトップ版で使えます。",
"ui.plugins_empty": "プラグインは入っていません。",
"ui.plugins_none": "検索に一致するプラグインはありません。",
"ui.plugins_note": "プラグインは {path} の中のフォルダで、plugin.json を置きます。SolidCraft はそのファイルを読むだけで、フォルダのコードは実行しません。オフにしてもインストールは残ります。",
"ui.plugins_search": "プラグインを検索",
"ui.plugins_skipped": "{count} 個のフォルダを読めませんでした。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2909,6 +2947,7 @@
"ui.project_face_edges_sketch": "新規スケッチに面のエッジを自動投影",
"ui.properties": "プロパティ",
"ui.pull_direction": "抜き方向",
"ui.quality": "品質",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "終了",
@@ -3015,6 +3054,7 @@
"ui.spline": "スプライン",
"ui.split_body": "ボディを分割",
"ui.stand_up_tip": "Z軸上向きで開いたSTEPまたはIGESファイルに対し、X軸周りに90度回転させるか確認します",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "最初からやり直す",
"ui.start_page": "スタートページ",
@@ -3124,6 +3164,7 @@
"update.prefs_flatpak": "このコピー ({version}) は Flatpak が更新します: {command} を実行するか、ソフトウェアセンターを使ってください。SolidCraft 自身は確認しません。",
"update.prefs_label": "アップデート",
"update.prefs_note": "これは SolidCraft {version} ({kind}) です。確認では GitHub に最新リリースを問い合わせるだけで、ほかには何も送信しません。",
"update.prefs_web": "ページを再読み込みすると、最新のウェブアプリを取得します。ネットワークが利用できない場合や、更新のダウンロードが時間内に完了しない場合は、保存済みのバージョンを開きます。進行中のダウンロードは次回の再読み込みに向けて続行されます。",
"update.release_page": "リリースページを開く",
"update.restart": "今すぐ再起動",
"update.restart_later": "後で",
@@ -3134,6 +3175,9 @@
"update.up_to_date": "SolidCraft は最新です。",
"update.update_now": "今すぐ更新",
"update.you_have": "現在のバージョンは {version} です。",
"web.open_full": "SolidCraft で開く",
"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 閉じる"
}
+5
View File
@@ -2013,6 +2013,8 @@
"ui.filter_commands_or_keys": "명령 또는 키 필터",
"ui.findInTimeline": "타임라인에서 찾기",
"ui.findInWindow": "창에서 찾기",
"ui.fine": "세밀",
"ui.finest": "가장 세밀",
"ui.finish_sketch": "스케치 종료",
"ui.fit": "맞춤",
"ui.fix_unfix": "고정/고정 해제",
@@ -2244,6 +2246,7 @@
"ui.project_face_edges_sketch": "면 모서리를 새 스케치로 투영",
"ui.properties": "속성",
"ui.pull_direction": "빼기 방향",
"ui.quality": "품질",
"ui.quantity": "수량",
"ui.quantity_2": "수량 2",
"ui.quit": "종료",
@@ -2344,6 +2347,7 @@
"ui.spline": "스플라인",
"ui.split_body": "바디 분할",
"ui.stand_up_tip": "STEP 또는 IGES 파일이 Z 위쪽으로 열린 후 X축 기준 90도 회전을 제안",
"ui.standard": "표준",
"ui.start": "시작",
"ui.start_over": "처음부터 다시",
"ui.start_page": "시작 페이지",
@@ -2446,6 +2450,7 @@
"update.prefs_flatpak": "이 사본({version})은 Flatpak이 업데이트합니다: {command}를 실행하거나 소프트웨어 센터를 사용하세요. SolidCraft는 직접 확인하지 않습니다.",
"update.prefs_label": "업데이트",
"update.prefs_note": "SolidCraft {version} ({kind})입니다. 확인 시 GitHub에 최신 릴리스만 요청하며 다른 정보는 보내지 않습니다.",
"update.prefs_web": "페이지를 새로고침하면 최신 웹 앱을 가져옵니다. 네트워크를 사용할 수 없거나 업데이트 다운로드가 제한 시간 안에 완료되지 않으면 저장된 버전이 열립니다. 진행 중인 다운로드는 다음 새로고침을 위해 계속됩니다.",
"update.release_page": "릴리스 페이지 열기",
"update.restart": "지금 다시 시작",
"update.restart_later": "나중에",
+4
View File
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
"ui.findInTimeline": "Cari dalam Garis Masa",
"ui.findInWindow": "Cari dalam Tetingkap",
"ui.fine": "Halus",
"ui.finest": "Paling halus",
"ui.finish_sketch": "Tamatkan Lakaran",
"ui.fit": "Muatkan",
"ui.fix_unfix": "Tetap/Lepas",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
"ui.properties": "Sifat",
"ui.pull_direction": "Arah tarikan",
"ui.quality": "Kualiti",
"ui.quantity": "Kuantiti",
"ui.quantity_2": "Kuantiti 2",
"ui.quit": "Keluar",
@@ -1910,6 +1913,7 @@
"ui.spline": "Splin",
"ui.split_body": "Belah Jasad",
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
"ui.standard": "Standard",
"ui.start": "Mula",
"ui.start_over": "Mula semula",
"ui.start_page": "Halaman mula",
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
"ui.findInTimeline": "Zoeken in tijdlijn",
"ui.findInWindow": "Zoeken in venster",
"ui.fine": "Fijn",
"ui.finest": "Zeer fijn",
"ui.finish_sketch": "Schets voltooien",
"ui.fit": "Passend",
"ui.fix_unfix": "Vastzetten/losmaken",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
"ui.properties": "Eigenschappen",
"ui.pull_direction": "Trekrichting",
"ui.quality": "Kwaliteit",
"ui.quantity": "Aantal",
"ui.quantity_2": "Aantal 2",
"ui.quit": "Afsluiten",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Lichaam splitsen",
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
"ui.standard": "Standaard",
"ui.start": "Start",
"ui.start_over": "Opnieuw beginnen",
"ui.start_page": "Startpagina",
+4
View File
@@ -1778,6 +1778,8 @@
"ui.filter_commands_or_keys": "Filtruj polecenia lub klawisze",
"ui.findInTimeline": "Znajdź na osi czasu",
"ui.findInWindow": "Znajdź w oknie",
"ui.fine": "Dokładna",
"ui.finest": "Najdokładniejsza",
"ui.finish_sketch": "Zakończ szkic",
"ui.fit": "Dopasuj",
"ui.fix_unfix": "Ustal/zwolnij",
@@ -2009,6 +2011,7 @@
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
"ui.properties": "Właściwości",
"ui.pull_direction": "Kierunek wyjmowania",
"ui.quality": "Jakość",
"ui.quantity": "Liczba",
"ui.quantity_2": "Liczba 2",
"ui.quit": "Zakończ",
@@ -2103,6 +2106,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Podziel bryłę",
"ui.stand_up_tip": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X",
"ui.standard": "Standardowa",
"ui.start": "Start",
"ui.start_over": "Zacznij od nowa",
"ui.start_page": "Strona startowa",
+331
View File
@@ -14,11 +14,15 @@
"Active": "Ativo",
"Active rule": "Regra ativa",
"Add": "Adicionar",
"Add Column": "Adicionar coluna",
"Add Key": "Adicionar chave",
"Add Mid Radius": "Adicionar raio intermediário",
"Add Row": "Adicionar linha",
"Add a tool": "Adicionar ferramenta",
"Add condition": "Adicionar condição",
"Add or edit rule": "Adicionar ou editar regra",
"Add to New Folder": "Adicionar à nova pasta",
"Advanced": "Avançado",
"Against": "Contra",
"Alignment": "Alinhamento",
"All Sketch Points": "Todos os pontos do esboço",
@@ -28,9 +32,11 @@
"Also deletes": "Também exclui",
"Angle 2": "Ângulo 2",
"Animate": "Animar",
"App/system information and command settings are included. Review private details before sharing.": "Informações do aplicativo/sistema e configurações estão incluídas. Revise detalhes privados antes de compartilhar.",
"Appearance name": "Nome da aparência",
"Applied Radius": "Raio aplicado",
"Apply To": "Aplicar a",
"Apply library appearance": "Aplicar aparência da biblioteca",
"Apply to selection": "Aplicar à seleção",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Aplique a aparência primeiro. Exporta as aparências atribuídas e os materiais criados neste projeto.",
"Arc length": "Comprimento do arco",
@@ -41,13 +47,18 @@
"BLEND SURFACE": "SUPERFÍCIE DE TRANSIÇÃO",
"BOUNDARY SURFACE": "SUPERFÍCIE DE CONTORNO",
"BROWSER": "NAVEGADOR",
"Bend Edges": "Arestas de dobra",
"Bend Line Position": "Posição da linha de dobra",
"Bend Position": "Posição da dobra",
"Bend Radius": "Raio de dobra",
"Bend Table": "Tabela de dobras",
"Bends": "Dobras",
"Bodies": "Corpos",
"Bodies (optional)": "Corpos (opcional)",
"Body": "Corpo",
"Body (last if none)": "Corpo (o último, se nenhum)",
"Body to split": "Corpo a dividir",
"Body, or faces to take out": "Corpo, ou faces a remover",
"Boundary": "Contorno",
"Bounding box": "Caixa delimitadora",
"CHECK": "VERIFICAR",
@@ -59,7 +70,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV ou JSON: atualiza parâmetros existentes e adiciona novos",
"Capture All Positions": "Capturar todas as posições",
"Capture Pos.": "Capturar pos.",
"Catch Depth": "Profundidade do encaixe",
"Catch Direction": "Direção do encaixe",
"Catch Length": "Comprimento do encaixe",
"Cells": "Células",
"Center X": "Centro X",
"Center Y": "Centro Y",
@@ -68,23 +81,31 @@
"Chamfer Type": "Tipo de chanfro",
"Check collisions": "Verificar colisões",
"Checked": "Marcado",
"Choose Variable radius or Spline in Type.": "Escolha Raio variável ou Spline em Tipo.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Escolha um novo plano. A geometria e as cotas do esboço permanecem inalteradas em suas coordenadas bidimensionais.",
"Choose…": "Escolher…",
"Circular": "Circular",
"Circumference": "Circunferência",
"Clockwise": "Horário",
"Close draft": "Fechar rascunho",
"Collapse": "Recolher",
"Collisions": "Colisões",
"Colour": "Cor",
"Command": "Comando",
"Comment": "Comentário",
"Component 1": "Componente 1",
"Component 2": "Componente 2",
"Component1": "Componente1",
"Components": "Componentes",
"Config Table": "Tabela de config.",
"Configuration": "Configuração",
"Configuration rules": "Regras de configuração",
"Contact": "Contato",
"Contact Sets": "Conjuntos de contato",
"Contact sets": "Conjuntos de contato",
"Continue on GitHub": "Continuar no GitHub",
"Continuity": "Continuidade",
"Control Radius": "Raio de controle",
"Copy Error Message": "Copiar mensagem de erro",
"Copy Warning Message": "Copiar mensagem de aviso",
"Copy all licences": "Copiar todas as licenças",
@@ -94,8 +115,11 @@
"Corner Relief": "Alívio de canto",
"Corner Relief Shape": "Forma do alívio de canto",
"Counterbore depth": "Profundidade do rebaixo",
"Counterbore Ø": "Ø do rebaixo",
"Countersink angle": "Ângulo do escareado",
"Countersink Ø": "Ø do escareado",
"Create Issue — review draft": "Criar issue — revisar rascunho",
"Create Issue…": "Criar issue…",
"Cross curves": "Curvas transversais",
"Cube": "Cubo",
"Current orthographic view (1:1 mm)": "Vista ortográfica atual (1:1 mm)",
@@ -103,18 +127,28 @@
"Curv. Comb": "Pente de curv.",
"Curvature (G2)": "Curvatura (G2)",
"Curvature change (G3)": "Variação de curvatura (G3)",
"Custom": "Personalizado",
"Customize Toolbar": "Personalizar barra de ferramentas",
"Customize…": "Personalizar…",
"Cylindrical face": "Face cilíndrica",
"Delete": "Excluir",
"Delete Selected Features…": "Excluir recursos selecionados…",
"Delete Selected Folders": "Excluir pastas selecionadas",
"Delete all": "Excluir tudo",
"Delete rule": "Excluir regra",
"Delete the row": "Excluir a linha",
"Delete the set": "Excluir o conjunto",
"Density": "Densidade",
"Dependents": "Dependentes",
"Describe the problem you saw": "Descreva o problema que você viu",
"Describe what happened before continuing.": "Descreva o que aconteceu antes de continuar.",
"Designs you open or save appear here.": "Os projetos que você abrir ou salvar aparecem aqui.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por Até o objeto. Escolha Ângulo para editar a extensão angular.",
"Diameter": "Diâmetro",
"Direction": "Direção",
"Discard": "Descartar",
"Distance": "Distância",
"Distance 1": "Distância 1",
"Do not export hidden edges": "Não exportar arestas ocultas",
"Don't ask again": "Não perguntar novamente",
"Double-click to rename": "Clique duas vezes para renomear",
@@ -126,49 +160,65 @@
"Edit Profile Sketch": "Editar esboço do perfil",
"Enable rule": "Ativar regra",
"Enabled": "Ativado",
"End Radius": "Raio final",
"End bulge": "Abaulamento final",
"End continuity": "Continuidade final",
"Expand": "Expandir",
"Expected result (optional)": "Resultado esperado (opcional)",
"Explode": "Explodir",
"Explode Text": "Decompor texto",
"Export Animation…": "Exportar animação…",
"Export Bend Table…": "Exportar tabela de dobras…",
"Export CSV…": "Exportar CSV…",
"Export Current View as DXF": "Exportar vista atual como DXF",
"Export Current View as DXF…": "Exportar vista atual como DXF…",
"Export DXF": "Exportar DXF",
"Export DXF…": "Exportar DXF…",
"Export JSON…": "Exportar JSON…",
"Export design library...": "Exportar biblioteca do projeto...",
"Export hidden edges as dashed lines": "Exportar arestas ocultas como linhas tracejadas",
"Exported by SolidWorks, which usually saves Y up": "Exportado pelo SolidWorks, que normalmente salva com Y para cima",
"Expression": "Expressão",
"Extend to meet": "Estender até encontrar",
"Extents": "Extensões",
"FILES": "ARQUIVOS",
"FLAT": "PLANIFICAÇÃO",
"Face": "Face",
"Face Appearance": "Aparência da face",
"Face attachment lost": "Fixação à face perdida",
"Faces": "Faces",
"Faces / edges / vertices": "Faces / arestas / vértices",
"Faces to split": "Faces a dividir",
"Fastener": "Fixador",
"Favorites": "Favoritos",
"Favourite": "Favorito",
"Feature": "Recurso",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Arquivos de muitos programas CAD usam Y para cima, então abrem deitados aqui (Z para cima).",
"Filter": "Filtro",
"Filter…": "Filtrar…",
"Find in Browser": "Localizar no navegador",
"Finish": "Acabamento",
"First plane": "Primeiro plano",
"Fit and G2 corners require Constant radius.": "Ajuste e cantos G2 exigem Raio constante.",
"Fixed Face": "Face fixa",
"Flat Pattern": "Planificação",
"Flat size": "Tamanho planificado",
"Follow camera (Look At sketches)": "Seguir a câmera (Olhar para os esboços)",
"Follow the camera (Look At sketches)": "Seguir a câmera (Olhar para os esboços)",
"Font": "Fonte",
"From": "De",
"From Bodies": "A partir de corpos",
"From edges": "Arestas de origem",
"Full Length": "Comprimento total",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "Cantos G2 exigem raio constante e Ajuste exato. A construção de cantos G2 ainda não é suportada.",
"Gap": "Folga",
"Grid": "Grade",
"HISTORY": "HISTÓRICO",
"Height": "Altura",
"Hidden edges": "Arestas ocultas",
"History layout": "Layout do histórico",
"History names": "Nomes no histórico",
"Hole (optional)": "Furo (opcional)",
"Holes": "Furos",
"Holes (edges on them)": "Furos (arestas neles)",
"Home view": "Vista inicial",
@@ -179,14 +229,20 @@
"In place (surfaces)": "No lugar (superfícies)",
"In plane": "No plano",
"Inside": "Dentro",
"Instant": "Instantâneo",
"Interface font": "Fonte da interface",
"Interferes": "Interfere",
"Invert": "Inverter",
"Isocurve": "Isocurva",
"Issue title": "Título da issue",
"Items": "Itens",
"JOINTS": "JUNTAS",
"Joint Origin1": "Origem da junta1",
"Keep (points on it)": "Manter (pontos sobre ela)",
"Keep Tools": "Manter ferramentas",
"Keep of the other surface": "Manter da outra superfície",
"Key a joint's value at this step": "Criar um quadro-chave com o valor de uma junta neste passo",
"Keys": "Quadros-chave",
"LIBRARY": "BIBLIOTECA",
"LINKS": "VÍNCULOS",
"Later": "Depois",
@@ -197,9 +253,14 @@
"Letter Spacing": "Espaçamento entre letras",
"Library": "Biblioteca",
"Library file": "Arquivo de biblioteca",
"Limits": "Limites",
"Line Spacing": "Espaçamento entre linhas",
"Line Type": "Tipo de linha",
"Listing the installed fonts…": "Listando as fontes instaladas…",
"Load library": "Carregar biblioteca",
"Local library": "Biblioteca local",
"Loop": "Laço",
"M5 or M5x0.5": "M5 ou M5x0.5",
"MANAGE": "GERENCIAR",
"MATERIALS": "MATERIAIS",
"MODEL": "MODELO",
@@ -210,14 +271,20 @@
"Matched restrictions check the final values.": "As restrições cujas condições são atendidas verificam os valores finais.",
"Material": "Material",
"Max": "Máx.",
"Maximum": "Máximo",
"Mesh body": "Corpo de malha",
"Mid Radius": "Raio intermediário",
"Middle double-click": "Clique duplo do meio",
"Min": "Mín.",
"Minimum": "Mínimo",
"Mirror Plane": "Plano de espelho",
"Mitre": "Meia-esquadria",
"Model Parameters": "Parâmetros do modelo",
"Modeled": "Modelado",
"More toolbar panels": "Mais painéis da barra de ferramentas",
"Motion": "Movimento",
"Mouse gestures": "Gestos do mouse",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Os gestos do mouse só funcionam no modo de mouse SolidWorks (a roda orbita, Ctrl + roda desloca).",
"Move / Copy": "Mover / Copiar",
"Move Type": "Tipo de movimento",
"Move down": "Mover para baixo",
@@ -235,15 +302,22 @@
"No matching appearance presets.": "Nenhuma predefinição de aparência correspondente.",
"No matching appearances": "Nenhuma aparência correspondente",
"No visible bodies or sketches to export.": "Nenhum corpo ou esboço visível para exportar.",
"Nominal diameter": "Diâmetro nominal",
"None": "Nenhum",
"Not available yet": "Ainda não disponível",
"Notch": "Entalhe",
"OUTPUT": "SAÍDA",
"Of": "De",
"On your computer": "No seu computador",
"Opacity": "Opacidade",
"Open GitHub draft": "Abrir rascunho no GitHub",
"Open library...": "Abrir biblioteca...",
"Open local libraries in the desktop app. You can export this design's library here.": "Abra bibliotecas locais no aplicativo para desktop. Você pode exportar a biblioteca deste projeto aqui.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "CAD 3D paramétrico de código aberto em Rust. Licenciado sob MIT OU Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Abre o navegador e copia o texto do relatório para você colar no GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Abre o navegador. Revise e envie seu relatório no GitHub.",
"Operator": "Operador",
"Orbit (right drag)": "Orbitar (arrastar com o botão direito)",
"Other side": "Outro lado",
"Other surface (mutual trim)": "Outra superfície (aparo mútuo)",
"Outside": "Fora",
@@ -258,8 +332,11 @@
"Pick the tool bodies": "Selecione os corpos de ferramenta",
"Picked": "Selecionado",
"Picked point": "Ponto selecionado",
"Pitch": "Passo",
"Pivot": "Pivô",
"Place at the origin instead": "Colocar na origem",
"Plastic Rules": "Regras de plástico",
"Play the joint's motion in the view": "Reproduzir o movimento da junta na vista",
"Playback": "Reprodução",
"Plot": "Gráfico",
"Position": "Posição",
@@ -267,14 +344,18 @@
"Position (G0)": "Posição (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Posição ao longo da aresta: 0 na extremidade inicial, 1 na outra",
"Preferences › Design turns the offer back on": "Preferências › Projeto reativa a oferta",
"Preview": "Visualização",
"Preview Row": "Visualizar linha",
"Preview leaves the current design unchanged.": "A visualização não altera o projeto atual.",
"Profile / Edges": "Perfil / Arestas",
"Profiles or curves": "Perfis ou curvas",
"Profiles/Curves": "Perfis/Curvas",
"Quantity": "Quantidade",
"RECENT": "RECENTES",
"REFERENCE": "REFERÊNCIA",
"RULED SURFACE": "SUPERFÍCIE REGRADA",
"RULES": "REGRAS",
"Radius": "Raio",
"Radius Fit": "Ajuste de raio",
"Ratio": "Razão",
"Reattach selected face": "Fixar novamente na face selecionada",
@@ -283,6 +364,8 @@
"Rectangular": "Retangular",
"Reference face": "Face de referência",
"Remove Tools": "Remover ferramentas",
"Remove condition": "Remover condição",
"Remove from Folder": "Remover da pasta",
"Remove from list": "Remover da lista",
"Remove override": "Remover substituição",
"Remove private design details or paths before sharing.": "Remova detalhes privados do projeto ou caminhos antes de compartilhar.",
@@ -290,12 +373,18 @@
"Remove the column": "Remover a coluna",
"Remove the track": "Remover a trilha",
"Remove this point": "Remover este ponto",
"Rename Feature": "Renomear recurso",
"Rename Folder": "Renomear pasta",
"Replace existing library file": "Substituir o arquivo de biblioteca existente",
"Report a bug — review a screenshot and open a GitHub draft": "Relatar um bug — revise uma captura de tela e abra um rascunho no GitHub",
"Report text copied. Paste it into GitHub and submit your report there.": "Texto do relatório copiado. Cole-o no GitHub e envie seu relatório por lá.",
"Reports are public. A GitHub account is required.": "Os relatórios são públicos. É necessária uma conta no GitHub.",
"Reset Toolbar": "Redefinir barra de ferramentas",
"Resize Part": "Redimensionar peça",
"Restore View": "Restaurar vista",
"Restore outer boundary": "Restaurar contorno externo",
"Restore the inherited component or physical material appearance": "Restaurar a aparência herdada do componente ou do material físico",
"Restore this studio's tools": "Restaurar as ferramentas deste ambiente",
"Result": "Resultado",
"Retake screenshot": "Refazer captura de tela",
"Reverse": "Inverter",
@@ -305,7 +394,14 @@
"Revert Pos.": "Reverter pos.",
"Revert Position": "Reverter posição",
"Review the report and screenshot before opening GitHub.": "Revise o relatório e a captura de tela antes de abrir o GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Revise seu relatório no GitHub e clique em Submit new issue para publicá-lo.",
"Rib Length": "Comprimento da nervura",
"Rib Offset": "Deslocamento da nervura",
"Rib Thickness": "Espessura da nervura",
"Ribs": "Nervuras",
"Rim Edge": "Aresta da borda",
"Ring of tools on the right button": "Anel de ferramentas no botão direito",
"Roll Back Here": "Retroceder até aqui",
"Roll History Marker Here": "Mover o marcador do histórico para cá",
"Round": "Arredondado",
"Rule": "Regra",
@@ -315,21 +411,28 @@
"SURFACE QUALITY": "QUALIDADE DA SUPERFÍCIE",
"Save Rule": "Salvar regra",
"Save View": "Salvar vista",
"Save screenshot…": "Salvar captura de tela…",
"Save the study as an animated GIF, seen as the viewport shows it": "Salvar o estudo como GIF animado, como a janela de visualização o mostra",
"Saved preview. Geometry is being checked before editing.": "Visualização salva. A geometria está sendo verificada antes da edição.",
"Scale factor": "Fator de escala",
"Scene": "Cena",
"Screenshot (optional)": "Captura de tela (opcional)",
"Search": "Pesquisar",
"Search commands": "Pesquisar comandos",
"Search this library": "Pesquisar nesta biblioteca",
"Second plane": "Segundo plano",
"Section": "Seção",
"Section position": "Posição da seção",
"Sections (one sketch each)": "Seções (um esboço cada)",
"Select Other": "Selecionar outro",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Selecionar outro: mantenha o botão esquerdo parado sobre o modelo (ou clique com o botão direito › Selecionar outro…) para listar tudo sob o cursor; clique de novo no mesmo ponto para selecionar o próximo item ali. Clique duas vezes com o botão do meio para ir ao Início (Preferências › Navegação).",
"Select bodies (a face picks its body)": "Selecione corpos (uma face seleciona seu corpo)",
"Select bodies or faces instead": "Selecionar corpos ou faces em vez disso",
"Select edges": "Selecione arestas",
"Selection filter": "Filtro de seleção",
"Semantic faces share their appearance across instances.": "Faces semânticas compartilham a aparência entre instâncias.",
"Send your report on GitHub": "Envie seu relatório no GitHub",
"Shaded": "Sombreado",
"Shape": "Forma",
"Sheet Rules": "Regras de chapa",
"Show Agent Cursor": "Mostrar cursor do agente",
@@ -348,6 +451,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "O SolidCraft fechou antes de estes projetos serem salvos. O último salvamento automático deles pode ser aberto.",
"Specialise": "Especializar",
"Speed": "Velocidade",
"Splitting tool": "Ferramenta de divisão",
"Square": "Quadrado",
"Stand it up": "Colocar em pé",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Colocar em pé: um quarto de volta em torno de X, como um recurso Mover que pode ser desfeito.",
@@ -357,17 +461,27 @@
"Start continuity": "Continuidade inicial",
"Step": "Passo",
"Steps": "Passos",
"Steps to reproduce (optional)": "Passos para reproduzir (opcional)",
"Stitch the kept pieces": "Costurar as partes mantidas",
"Stitch to both bodies": "Costurar aos dois corpos",
"Studio": "Ambiente",
"Study": "Estudo",
"Style": "Estilo",
"Summary": "Resumo",
"Suppress": "Suprimir",
"Suppressed": "Suprimido",
"Surface": "Superfície",
"Surface, or faces to turn over": "Superfície, ou faces a inverter",
"Swap Ends": "Trocar extremidades",
"TRIM": "APARAR",
"Tangent (G1)": "Tangente (G1)",
"Tangent Chain": "Cadeia tangente",
"Tangent chain": "Cadeia tangente",
"Tap drill diameter": "Diâmetro da broca para macho",
"Target": "Alvo",
"Target Body": "Corpo-alvo",
"Tear": "Rasgo",
"Technical details": "Detalhes técnicos",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "O esboço tracejado é uma guia de plano salva. Selecione uma nova face para reparar a fixação.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "A ocorrência de autoria salva não existe mais. Selecione um posicionamento de autoria válido antes de vincular novamente.",
"Then": "Então",
@@ -379,24 +493,32 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Este filho é compartilhado. Excluí-lo o remove de todas as cópias do pai.",
"This child is shared. Position changes apply to every copy of its parent.": "Este filho é compartilhado. Mudanças de posição se aplicam a todas as cópias do pai.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta face não pode fornecer uma fixação duradoura. Escolha outra face ou use explicitamente um plano fixo.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Esta imagem ainda não está anexada no GitHub. Copie ou salve-a e depois anexe-a no GitHub.",
"To": "Para",
"To Offset": "Deslocamento final",
"To edges": "Arestas de destino",
"Tolerance": "Tolerância",
"Too close": "Muito próximo",
"Tool Bodies": "Corpos-ferramenta",
"Tools": "Ferramentas",
"Tools, clockwise from the top": "Ferramentas, em sentido horário a partir do topo",
"Trimming tool (plane, face or curves)": "Ferramenta de aparo (plano, face ou curvas)",
"Turn off section analysis before exporting the view.": "Desative a análise de seção antes de exportar a vista.",
"Type": "Tipo",
"UNTRIM": "DESAPARAR",
"Unit": "Unidade",
"Unsuppress": "Reativar",
"Updating the preview": "Atualizando a visualização",
"Use active author placement": "Usar o posicionamento de autoria ativo",
"Use fixed plane": "Usar plano fixo",
"Use saved fixed plane": "Usar o plano fixo salvo",
"Use selected face as fixed plane": "Usar a face selecionada como plano fixo",
"Use the SolidWorks mouse mode": "Usar o modo de mouse SolidWorks",
"User Parameters": "Parâmetros do usuário",
"VIEW": "VISTA",
"VIEWS": "VISTAS",
"Value": "Valor",
"Variable Radius": "Raio variável",
"Velocity": "Velocidade",
"Vertices": "Vértices",
"View navigator": "Navegador de vistas",
@@ -404,16 +526,50 @@
"Wall": "Parede",
"Wall Location": "Posição da parede",
"Wall Thickness": "Espessura da parede",
"What did you do just before the problem? A short list is enough": "O que você fez logo antes do problema? Uma lista curta basta",
"What did you expect to see?": "O que você esperava ver?",
"What happened?": "O que aconteceu?",
"When": "Quando",
"Width": "Largura",
"Will fail": "Vai falhar",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Com os gestos ativados, o botão direito não orbita mais a vista: segure-o e mova para abrir o anel, depois toque na zona de uma ferramenta. Um clique direito simples ainda abre o menu.",
"X": "X",
"X Angle": "Ângulo X",
"X angle": "Ângulo X",
"X distance": "Distância X",
"Y": "Y",
"Y Angle": "Ângulo Y",
"Y angle": "Ângulo Y",
"Y distance": "Distância Y",
"Z": "Z",
"Z Angle": "Ângulo Z",
"Z Distance": "Distância Z",
"Z angle": "Ângulo Z",
"Z distance": "Distância Z",
"Zebra": "Zebra",
"ai.prefs.availability": "Controles do Assistente",
"ai.prefs.background_commands": "Executar comandos do agente em segundo plano",
"ai.prefs.follow_camera": "Virar a câmera para os esboços",
"ai.prefs.follow_camera_off": "O acompanhamento automático da câmera está desativado.",
"ai.prefs.follow_camera_on": "A câmera se vira para os esboços que o agente cria.",
"ai.prefs.intro": "Agentes, permissões e como o trabalho deles aparece.",
"ai.prefs.make_default": "Usar como padrão",
"ai.prefs.no_agents": "Nenhum agente ainda. Adicione um agente ou atualize para encontrar agentes instalados.",
"ai.prefs.no_saved_approvals": "Nenhuma aprovação salva.",
"ai.prefs.override_all_warning": "Este agente pode salvar, sobrescrever, exportar ou substituir o projeto sem perguntar.",
"ai.prefs.permission_all": "Permitir tudo",
"ai.prefs.permission_all_summary": "As ferramentas disponíveis do SolidCraft são executadas sem perguntar.",
"ai.prefs.permission_ask": "Perguntar antes de alterar",
"ai.prefs.permission_ask_summary": "Aprovar cada alteração no projeto e cada ação de arquivo.",
"ai.prefs.permission_edits": "Permitir edições no modelo",
"ai.prefs.permission_edits_summary": "Edições no modelo são executadas; ações de arquivo e destrutivas ainda perguntam.",
"ai.prefs.permission_scope": "Só para ferramentas do SolidCraft. O shell e as edições de arquivos do agente usam as permissões dele.",
"ai.prefs.pointer_preview_hidden": "Ponteiro oculto",
"ai.prefs.pointer_preview_speed": "Ponteiro do agente · {speed}",
"ai.prefs.pointer_speed": "Velocidade do movimento",
"ai.prefs.privacy_note": "Só o agente ou endpoint que você escolher recebe a conversa. As chaves de endpoint ficam guardadas no chaveiro do sistema.",
"ai.prefs.show_pointer": "Mostrar ponteiro do agente",
"ai.prefs.turn_limit_hint": "Número máximo de chamadas de ferramenta que alteram o projeto que o Assistente pode fazer por mensagem.",
"angle, height": "ângulo, altura",
"appearance.dielectric_ior": "IOR dielétrico",
"appearance.metallic": "Metálico",
@@ -460,8 +616,10 @@
"cmd.component_activate": "Ativar componente",
"cmd.component_break_link": "Quebrar vínculo",
"cmd.component_capture_position": "Capturar posição",
"cmd.component_copies": "Encontrar cópias idênticas",
"cmd.component_create": "Novo componente",
"cmd.component_delete": "Excluir componente",
"cmd.component_extract": "Extrair para arquivo",
"cmd.component_from_bodies": "Criar componentes a partir de corpos",
"cmd.component_insert_design": "Inserir projeto",
"cmd.component_links": "Listar vínculos",
@@ -474,6 +632,7 @@
"cmd.component_properties": "Propriedades físicas do componente",
"cmd.component_relink": "Revincular origem",
"cmd.component_rename": "Renomear componente",
"cmd.component_replace_with_link": "Substituir por vínculo",
"cmd.component_revert_position": "Reverter posição",
"cmd.component_unlock": "Desbloquear conteúdo",
"cmd.component_update_links": "Atualizar vínculos",
@@ -596,6 +755,7 @@
"cmd.motion_plot": "Plotar estudo de movimento",
"cmd.motion_study": "Estudo de movimento",
"cmd.motion_trace": "Traçado de movimento",
"cmd.motion_trace_edit": "Editar traçado de movimento",
"cmd.motion_trace_keep": "Manter traçado de movimento",
"cmd.occurrence_copy": "Colar (instância)",
"cmd.occurrence_ground": "Fixar no solo",
@@ -775,6 +935,7 @@
"cmd.view_left": "Vista esquerda",
"cmd.view_list": "Listar vistas nomeadas",
"cmd.view_navigate": "Navegar na vista",
"cmd.view_normal_to": "Normal à seleção",
"cmd.view_rename": "Renomear vista nomeada",
"cmd.view_right": "Vista direita",
"cmd.view_save": "Nova vista nomeada",
@@ -874,6 +1035,12 @@
"help.licences": "Licenças",
"help.mit": "Licença MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Corta as arestas selecionadas com um bisel plano na distância que você definir. Use para quebrar arestas vivas ou facilitar a entrada de um pino ou parafuso.",
"help.solid.extrude": "Empurra um perfil de esboço para o 3D ao longo de uma distância, adicionando ou removendo material. Use para transformar um contorno plano em um bloco, um ressalto ou um rebaixo.",
"help.solid.fillet": "Arredonda as arestas selecionadas com uma concordância circular suave do raio que você definir. Use para suavizar arestas vivas, por resistência, conforto ou aparência.",
"help.solid.hole": "Faz um furo redondo do diâmetro que você definir em um ponto escolhido de uma face. Use para folgas de parafuso, pinos e outras aberturas redondas simples.",
"help.solid.revolve": "Gira um perfil de esboço em torno de um eixo para criar um sólido redondo. Use para eixos, botões, tigelas e qualquer peça redonda em torno de uma linha central.",
"help.solid.shell": "Esvazia um sólido, deixando paredes da espessura que você definir, e pode abrir as faces que você escolher. Use para caixas, tampas e outras peças de parede fina.",
"help.third_party": "Licenças de terceiros",
"home.back": "Voltar para {name}",
"home.crashes": "Relatórios de falhas",
@@ -983,6 +1150,15 @@
"motion.to_end": "Para o fim",
"motion.to_start": "Para o início",
"motion.track_name": "Trilha {number}",
"nav.alias.help": "Shift+Alt+arrastar com o esquerdo orbita, Shift+Alt+meio desloca, Shift+Alt+direito aplica zoom; arrastar com o meio também desloca e arrastar com o direito orbita",
"nav.blender.help": "Arrastar com o meio orbita, Shift+meio desloca, Ctrl+meio aplica zoom; arrastar com o direito não mexe na câmera",
"nav.free_orbit.help": "O mouse e o mouse 3D giram passando pelos dois polos e preservam o giro lateral (roll). O encaixe em eixo com Shift fica desativado.",
"nav.free_orbit.label": "Órbita livre",
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.openscad.help": "Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
"nav.solidworks.help": "Arrastar com o meio orbita, Ctrl+meio desloca, Shift+meio aplica zoom, arrastar com o direito orbita",
"nav.tinkercad.help": "Arrastar com o direito orbita, arrastar com o meio ou Shift+direito desloca",
"nav.touchpad.help": "Segure Shift e mova o ponteiro para deslocar; segure Alt/Option e mova para orbitar. Não é preciso botão do mouse. A rolagem vertical aplica zoom.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Orbitar (segure Alt/Option e mova; sem botão do mouse)",
@@ -1005,8 +1181,130 @@
"pattern.flip_direction": "Inverter direção",
"pick a hole to seat it, or OK places it at the origin": "selecione um furo para assentá-la, ou OK a coloca na origem",
"pick one or two vertices, edges or faces": "selecione um ou dois vértices, arestas ou faces",
"prefs.add_item": "Adicionar o item",
"prefs.alt_frees_manipulators": "Alt / Option também libera as setas do manipulador",
"prefs.anti_aliasing_note": "Suaviza bordas serrilhadas.",
"prefs.applies_on_restart": "Aplica-se ao reiniciar",
"prefs.assistant": "Assistente",
"prefs.axes": "Eixos",
"prefs.axis": "Eixo",
"prefs.bottom": "Embaixo",
"prefs.browser": "Navegador",
"prefs.built_in": "Integrado",
"prefs.button_left": "Arrastar com o esquerdo",
"prefs.button_middle": "Arrastar com o meio",
"prefs.button_right": "Arrastar com o direito",
"prefs.choose_font": "Escolher arquivo de fonte…",
"prefs.click": "Clique",
"prefs.contact_shading": "Sombreamento suave de contato",
"prefs.contact_shading_note": "Sombreamento suave onde as peças se aproximam.",
"prefs.context_menu": "Menu de contexto",
"prefs.context_menu_ring": "Menu de contexto · segure para o menu radial",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Abrir relatórios de falhas…",
"prefs.crash_reports_note": "Os relatórios ficam guardados neste computador, com o projeto e os comandos que reproduzem o problema.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Superfícies curvas",
"prefs.curves": "Superfícies curvas",
"prefs.dead_zone_note": "A faixa hachurada é a zona morta: o movimento dentro dela é ignorado.",
"prefs.default": "padrão",
"prefs.design": "Padrões do projeto",
"prefs.design_note": "Com o que os novos projetos começam. Projetos abertos mantêm as próprias configurações.",
"prefs.devices": "Mouse 3D",
"prefs.display": "Exibição",
"prefs.edges": "Arestas e sombreamento",
"prefs.extend": "Extensões",
"prefs.extra_navigation": "Ctrl+arrastar com o esquerdo no espaço vazio orbita; segure Espaço para deslocar.",
"prefs.follows_os": "Segue o sistema",
"prefs.font": "Fonte",
"prefs.font_fallback": "Os glifos que faltam na fonte escolhida usam a fonte integrada, então nenhum rótulo mostra uma caixa vazia.",
"prefs.font_sample": "Vovô viu o ímã, a maçã e o pinguim · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Não foi possível ler a fonte selecionada.",
"prefs.general": "Geral",
"prefs.general_note": "Idioma, salvamento e atualizações.",
"prefs.gesture_orbit": "Orbitar",
"prefs.gesture_pan": "Deslocar",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Gráficos",
"prefs.graphics_note": "Troque velocidade por suavidade. Cada opção mostra seu efeito em uma amostra.",
"prefs.graphics_presets_note": "As predefinições ajustam as opções abaixo em conjunto.",
"prefs.history": "Histórico",
"prefs.hold_stop_snapping": "Segure para desativar o encaixe",
"prefs.icons_only": "Só ícones",
"prefs.imported_file": "Arquivo importado",
"prefs.input": "Entrada",
"prefs.interface": "Interface",
"prefs.interface_language": "Idioma da interface",
"prefs.interface_note": "A aparência do SolidCraft e onde ficam suas partes.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Rótulos",
"prefs.layout": "Layout",
"prefs.layout_note": "Compacto encaixa os painéis nas bordas da janela. A translucidez do navegador está disponível em Flutuante. Mude o lado do Assistente na página Assistente.",
"prefs.left": "Esquerda",
"prefs.left_click": "Clique esquerdo",
"prefs.live_input": "Entrada ao vivo",
"prefs.live_preview": "Visualização ao vivo",
"prefs.minutes": "minutos",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mouse e trackpad",
"prefs.mouse_heading": "Mouse",
"prefs.mouse_mapping": "Mapeamento do mouse",
"prefs.navigation": "Navegação",
"prefs.navigation_note": "Como o mouse, o trackpad e o mouse 3D movem a vista.",
"prefs.not_on_gpu": "indisponível nesta GPU",
"prefs.on": "Ativado",
"prefs.opaque": "Opaco",
"prefs.orbit_snap": "Segure Shift ao orbitar para encaixar em um eixo.",
"prefs.orientation": "Orientação",
"prefs.overall_speed": "Velocidade geral",
"prefs.preview_choices": "Prévia das suas escolhas",
"prefs.quality_default": "padrão",
"prefs.quality_fastest": "mais rápido",
"prefs.quality_slowest": "mais lento",
"prefs.quality_smoother": "mais suave",
"prefs.reports_saved": "{n} salvos",
"prefs.right_click": "Clique direito",
"prefs.saving": "Salvamento automático e recuperação",
"prefs.scale_note": "Também Ctrl + / Ctrl − na janela. Os botões têm 28 px em 100%.",
"prefs.scale_text": "Escala e texto",
"prefs.select": "Selecionar",
"prefs.select_one_item": "Selecionar um item",
"prefs.selecting": "Seleção",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift só adiciona; Ctrl / Cmd alterna",
"prefs.shift_add_note": "Ativado: Shift adiciona, Ctrl / Cmd alterna. Desativado (padrão): ambos alternam.",
"prefs.shortcuts": "Atalhos",
"prefs.sketch_selection": "Esboço e seleção",
"prefs.sketch_selection_heading": "Esboço e seleção",
"prefs.sketch_selection_note": "Como os esboços encaixam e como os cliques selecionam.",
"prefs.sketching": "Criação de esboços",
"prefs.snap_drawing_note": "Ao desenhar ou arrastar um ponto do esboço.",
"prefs.source": "Origem",
"prefs.speed": "Velocidade",
"prefs.stand_up_note": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X.",
"prefs.status_done": "Concluído",
"prefs.status_error": "Erro",
"prefs.status_selected": "Selecionado",
"prefs.status_warning": "Aviso",
"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.timeline_half_hour": "−30 min",
"prefs.timeline_now": "agora",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Adicionar o item, ou retirá-lo de novo",
"prefs.toolbar": "Barra de ferramentas",
"prefs.translucent": "Translúcido",
"prefs.under_browser": "Abaixo do navegador",
"prefs.under_cursor": "Sob o cursor",
"prefs.units_precision": "Unidades e precisão",
"prefs.updates": "Atualizações",
"prefs.view": "Comportamento da vista",
"prefs.viewport": "Janela de visualização",
"prefs.wheel": "Roda",
"prefs.wheel_trackpad": "Roda e trackpad",
"prefs.workspace": "Espaço de trabalho",
"prefs.zoom_toward_cursor": "Zoom em direção ao cursor",
"properties.title": "Propriedades: {name}",
"recovery.days_ago": "há {count} dias",
"recovery.detail": "{file} · {features} recursos · salvo automaticamente {when}",
@@ -1084,9 +1382,12 @@
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
"settings.msaa_reset": "{setting}: {requested}× não é suportado por este dispositivo gráfico; agora é {reset}× (suportados: {supported}).",
"settings.msaa_unsupported": "Não suportado por este dispositivo gráfico",
"settings.reference_geometry_through_bodies": "Mostrar geometria de referência através dos corpos",
"settings.reference_geometry_through_bodies_help": "Desenhar contornos de esboços inativos e planos de referência sobre os corpos sombreados",
"settings.reset_intro": "Estas configurações não funcionam neste computador e impediriam o SolidCraft de iniciar, por isso foram alteradas:",
"settings.reset_title": "As configurações foram alteradas",
"settings.supersampling_note": "Desenha a vista em resolução maior e a filtra para baixo: arestas, linhas e grade mais suaves em telas abaixo de 4K. Custa tempo de GPU: cerca de 2,25× os pixels em 1,5× e 4× em 2×.",
"sheet.no_body": "não há corpo de chapa metálica: comece um com Flange > Base, ou transforme uma placa em chapa metálica com Converter em chapa metálica (selecione a face plana grande dela)",
"shortcut.json_error": "os parâmetros devem estar no formato JSON: {error}",
"shortcut.no_command": "o comando `{id}` não existe; o texto antes do JSON deve ser um ID de comando",
"shortcut.pin": "Fixar no topo da caixa S",
@@ -1123,6 +1424,9 @@
"thread.designation": "Designação",
"thread.iso_metric": "Métrica ISO",
"thread.select_size": "Selecionar tamanho",
"timeline.delete_after_marker_one": "Excluir 1 recurso após o marcador do histórico",
"timeline.delete_after_marker_other": "Excluir {count} recursos após o marcador do histórico",
"timeline.delete_many": "Excluir {count} recursos selecionados",
"timeline.group": "Nova pasta de recursos",
"timeline.move_many": "Mover itens da linha do tempo",
"timeline.move_to_group": "Mover recursos para a pasta",
@@ -1368,11 +1672,15 @@
"ui.agent_chat_waiting": "Aguardando sua resposta",
"ui.agent_cursor": "Cursor do agente",
"ui.agent_cursor_speed": "Velocidade do cursor do agente",
"ui.agent_off_thread": "Manter a janela respondendo enquanto os agentes executam comandos",
"ui.agent_off_thread_note": "Os comandos lentos de um agente (do painel de IA ou de um cliente MCP) são calculados ao lado da janela: você pode continuar girando e inspecionando a vista, e parar um deles pelo indicador na vista. Desativado: cada um roda na thread da janela, e a janela espera ele terminar.",
"ui.ai_add": "Adicionar",
"ui.ai_add_agent": "Adicionar agente…",
"ui.ai_add_agent_title": "Adicionar agente",
"ui.ai_add_anthropic": "API compatível com Anthropic",
"ui.ai_add_api": "API compatível com OpenAI",
"ui.ai_add_custom": "Comando personalizado",
"ui.ai_add_endpoint": "Endpoint de API",
"ui.ai_add_installed": "Agente instalado",
"ui.ai_add_installed_note": "Uma entrada própria para um agente instalado, por exemplo com outros argumentos. Os agentes instalados são listados por si mesmos.",
"ui.ai_added": "adicionado",
@@ -1401,6 +1709,7 @@
"ui.ai_key_needs_https": "Uma chave só é enviada por HTTPS ou para este computador.",
"ui.ai_key_note": "Guardada no chaveiro do sistema, nunca nas configurações do SolidCraft. Deixe em branco para um servidor local.",
"ui.ai_keychain_error": "O chaveiro do sistema recusou: {error}",
"ui.ai_kind": "Tipo",
"ui.ai_mode_all": "Permitir tudo",
"ui.ai_mode_all_note": "Nada pergunta antes. Os relatórios de falha continuam fora de alcance e os limites de pasta ainda valem.",
"ui.ai_mode_all_warning": "O agente pode salvar, sobrescrever, exportar e substituir seu projeto sem perguntar. Use isto só com um agente em que você confie e em projetos que você já salvou.",
@@ -1422,6 +1731,7 @@
"ui.ai_panel": "Painel",
"ui.ai_permissions": "Permissões",
"ui.ai_preset": "Predefinição",
"ui.ai_protocol": "Protocolo",
"ui.ai_remove": "Remover",
"ui.ai_reset": "Redefinir",
"ui.ai_settings": "Configurações de IA…",
@@ -1431,6 +1741,8 @@
"ui.ai_test": "Testar",
"ui.ai_test_acp_ok": "Responde: {agent}",
"ui.ai_test_acp_tip": "Inicia o agente por um instante e pede uma sessão; depois o encerra",
"ui.ai_test_answered": "Acessado: {model} respondeu",
"ui.ai_test_anthropic_tip": "Envia uma mensagem curta ao endpoint",
"ui.ai_test_api_tip": "Pede ao endpoint a lista de modelos",
"ui.ai_test_no_model": "Acessado: {count} modelos, mas não {model}",
"ui.ai_test_ok": "Acessado: {count} modelos",
@@ -1589,6 +1901,9 @@
"ui.export_keymap_tip": "Teclas, favoritos da caixa S e a barra de ferramentas, como um arquivo",
"ui.extend": "Estender",
"ui.extent_type": "Tipo de extensão",
"ui.extract_component": "Extrair para arquivo…",
"ui.extract_design_changed": "O projeto mudou enquanto um arquivo era escolhido. Inicie Extrair para arquivo de novo.",
"ui.extract_done": "O componente agora é um projeto próprio, vinculado aqui.",
"ui.extrude": "Extrudar",
"ui.factor": "Fator",
"ui.feature_history": "Histórico de recursos",
@@ -1599,6 +1914,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos ou teclas",
"ui.findInTimeline": "Localizar na linha do tempo",
"ui.findInWindow": "Localizar na janela",
"ui.fine": "Fina",
"ui.finest": "Muito fina",
"ui.finish_sketch": "Concluir esboço",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1696,10 +2013,17 @@
"ui.hideProfile": "Ocultar perfil",
"ui.history_back": "Voltar uma etapa",
"ui.history_below_browser": "Abaixo do navegador",
"ui.history_edge_hint": "clique duas vezes na borda",
"ui.history_first": "Ir para o início",
"ui.history_forward": "Avançar uma etapa",
"ui.history_hide_names": "Ocultar nomes",
"ui.history_last": "Ir para o fim",
"ui.history_marker_tip": "Marcador do histórico: arraste para retroceder o modelo; novos recursos entram aqui",
"ui.history_names": "Nomes",
"ui.history_play": "Reproduzir até o fim",
"ui.history_show_names": "Mostrar nomes",
"ui.history_title": "Histórico",
"ui.history_toggle_names": "Mostrar ou ocultar os nomes dos recursos",
"ui.hole": "Furo",
"ui.hole_tap_type": "Tipo de rosca do furo",
"ui.hole_type": "Tipo de furo",
@@ -1721,6 +2045,10 @@
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
"ui.link_copies": "Vincular cópias idênticas",
"ui.link_copies_keep": "Manter",
"ui.link_copies_link": "Vincular",
"ui.link_copies_text": "Estes componentes constroem os mesmos corpos que {name}. Torná-los posicionamentos do vínculo dele?",
"ui.lock": "Bloquear",
"ui.loft": "Loft",
"ui.look_at": "Olhar para",
@@ -1823,6 +2151,7 @@
"ui.project_face_edges_sketch": "Projetar arestas da face em novos esboços",
"ui.properties": "Propriedades",
"ui.pull_direction": "Direção de extração",
"ui.quality": "Qualidade",
"ui.quantity": "Quantidade",
"ui.quantity_2": "Quantidade 2",
"ui.quit": "Sair",
@@ -1844,6 +2173,7 @@
"ui.render": "Renderizar",
"ui.renderer": "Renderizador",
"ui.repeat": "Repetir",
"ui.replace_with_link_done": "As cópias agora são posicionamentos do projeto vinculado.",
"ui.report_a_bug": "Relatar um bug",
"ui.reset": "Redefinir",
"ui.reset_all": "Redefinir tudo",
@@ -1917,6 +2247,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir corpo",
"ui.stand_up_tip": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X",
"ui.standard": "Padrão",
"ui.start": "Início",
"ui.start_over": "Recomeçar",
"ui.start_page": "Página inicial",
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filtrează comenzi sau taste",
"ui.findInTimeline": "Găsește în cronologie",
"ui.findInWindow": "Găsește în fereastră",
"ui.fine": "Fină",
"ui.finest": "Foarte fină",
"ui.finish_sketch": "Finalizează schița",
"ui.fit": "Potrivire",
"ui.fix_unfix": "Fixează/Defixează",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Proiectează muchiile feței în schițe noi",
"ui.properties": "Proprietăți",
"ui.pull_direction": "Direcția de extragere",
"ui.quality": "Calitate",
"ui.quantity": "Cantitate",
"ui.quantity_2": "Cantitatea 2",
"ui.quit": "Ieșire",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Împarte corpul",
"ui.stand_up_tip": "După ce un fișier STEP sau IGES se deschide cu Z în sus, oferă un sfert de rotație în jurul lui X",
"ui.standard": "Standard",
"ui.start": "Început",
"ui.start_over": "Reîncepe",
"ui.start_page": "Pagina de start",
+4
View File
@@ -1591,6 +1591,8 @@
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
"ui.findInTimeline": "Найти на шкале",
"ui.findInWindow": "Найти в окне",
"ui.fine": "Высокое",
"ui.finest": "Максимальное",
"ui.finish_sketch": "Завершить эскиз",
"ui.fit": "Вписать",
"ui.fix_unfix": "Зафиксировать/Освободить",
@@ -1815,6 +1817,7 @@
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
"ui.properties": "Свойства",
"ui.pull_direction": "Направление вытягивания",
"ui.quality": "Качество",
"ui.quantity": "Количество",
"ui.quantity_2": "Количество 2",
"ui.quit": "Выход",
@@ -1909,6 +1912,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Разделить тело",
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
"ui.standard": "Стандартное",
"ui.start": "Старт",
"ui.start_over": "Начать заново",
"ui.start_page": "Начальная страница",
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filtrera kommandon eller tangenter",
"ui.findInTimeline": "Sök i tidslinjen",
"ui.findInWindow": "Sök i fönstret",
"ui.fine": "Fin",
"ui.finest": "Finast",
"ui.finish_sketch": "Avsluta skiss",
"ui.fit": "Anpassa",
"ui.fix_unfix": "Fäst/Lös",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Projicera ytkanter i nya skisser",
"ui.properties": "Egenskaper",
"ui.pull_direction": "Dragriktning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Avsluta",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dela kropp",
"ui.stand_up_tip": "När en STEP- eller IGES-fil öppnas med Z uppåt, erbjud ett kvartsvarv runt X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Börja om",
"ui.start_page": "Startsida",
+4
View File
@@ -1591,6 +1591,8 @@
"ui.filter_commands_or_keys": "Komutları veya tuşları filtrele",
"ui.findInTimeline": "Zaman çizelgesinde bul",
"ui.findInWindow": "Pencerede bul",
"ui.fine": "İnce",
"ui.finest": "En ince",
"ui.finish_sketch": "Eskizi bitir",
"ui.fit": "Sığdır",
"ui.fix_unfix": "Sabitle/Çöz",
@@ -1815,6 +1817,7 @@
"ui.project_face_edges_sketch": "Yüz kenarlarını yeni eskizlere yansıt",
"ui.properties": "Özellikler",
"ui.pull_direction": "Kalıptan çıkarma yönü",
"ui.quality": "Kalite",
"ui.quantity": "Adet",
"ui.quantity_2": "Adet 2",
"ui.quit": "Çıkış",
@@ -1909,6 +1912,7 @@
"ui.spline": "Spline",
"ui.split_body": "Gövdeyi böl",
"ui.stand_up_tip": "Bir STEP veya IGES dosyası Z yukarı olarak açıldığında X etrafında çeyrek tur döndürmeyi öner",
"ui.standard": "Standart",
"ui.start": "Başlangıç",
"ui.start_over": "Baştan başla",
"ui.start_page": "Başlangıç sayfası",
+307 -1
View File
@@ -14,11 +14,15 @@
"Active": "Активна",
"Active rule": "Активне правило",
"Add": "Додати",
"Add Key": "Додати клавішу",
"Add Column": "Додати стовпець",
"Add Key": "Додати ключовий кадр",
"Add Mid Radius": "Додати середній радіус",
"Add Row": "Додати рядок",
"Add a tool": "Додати інструмент",
"Add condition": "Додати умову",
"Add or edit rule": "Додати або змінити правило",
"Add to New Folder": "Додати до нової теки",
"Advanced": "Додатково",
"Against": "Проти",
"Alignment": "Вирівнювання",
"All Sketch Points": "Усі точки ескізу",
@@ -28,9 +32,11 @@
"Also deletes": "Також видаляє",
"Angle 2": "Кут 2",
"Animate": "Анімувати",
"App/system information and command settings are included. Review private details before sharing.": "Додано відомості про програму й систему та налаштування команд. Перегляньте приватні дані перед поширенням.",
"Appearance name": "Назва вигляду",
"Applied Radius": "Застосований радіус",
"Apply To": "Застосувати до",
"Apply library appearance": "Застосувати вигляд із бібліотеки",
"Apply to selection": "Застосувати до виділення",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Спочатку застосуйте вигляд. Експортує призначені вигляди та матеріали, створені в цьому проєкті.",
"Arc length": "Довжина дуги",
@@ -41,13 +47,18 @@
"BLEND SURFACE": "ПОВЕРХНЯ ПЕРЕХОДУ",
"BOUNDARY SURFACE": "ПОВЕРХНЯ ЗА МЕЖАМИ",
"BROWSER": "БРАУЗЕР",
"Bend Edges": "Ребра згину",
"Bend Line Position": "Положення лінії згину",
"Bend Position": "Положення згину",
"Bend Radius": "Радіус згину",
"Bend Table": "Таблиця згинів",
"Bends": "Згини",
"Bodies": "Тіла",
"Bodies (optional)": "Тіла (необов'язково)",
"Body": "Тіло",
"Body (last if none)": "Тіло (останнє, якщо не вибрано)",
"Body to split": "Тіло для розділення",
"Body, or faces to take out": "Тіло або грані для вилучення",
"Boundary": "Межа",
"Bounding box": "Обмежувальна рамка",
"CHECK": "ПЕРЕВІРКА",
@@ -59,7 +70,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV або JSON: оновлює наявні параметри, додає нові",
"Capture All Positions": "Зафіксувати всі положення",
"Capture Pos.": "Зафікс. полож.",
"Catch Depth": "Глибина зачеплення",
"Catch Direction": "Напрямок зачеплення",
"Catch Length": "Довжина зачеплення",
"Cells": "Комірки",
"Center X": "Центр X",
"Center Y": "Центр Y",
@@ -68,23 +81,31 @@
"Chamfer Type": "Тип фаски",
"Check collisions": "Перевіряти зіткнення",
"Checked": "Перевірено",
"Choose Variable radius or Spline in Type.": "Виберіть у полі «Тип» значення «Variable radius» або «Spline».",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Виберіть нову площину. Геометрія та розміри ескізу залишаються незмінними у своїх двовимірних координатах.",
"Choose…": "Вибрати…",
"Circular": "Круговий",
"Circumference": "Довжина кола",
"Clockwise": "За годинниковою стрілкою",
"Close draft": "Закрити чернетку",
"Collapse": "Згорнути",
"Collisions": "Зіткнення",
"Colour": "Колір",
"Command": "Команда",
"Comment": "Коментар",
"Component 1": "Компонент 1",
"Component 2": "Компонент 2",
"Component1": "Компонент1",
"Components": "Компоненти",
"Config Table": "Табл. конфігурацій",
"Configuration": "Конфігурація",
"Configuration rules": "Правила конфігурації",
"Contact": "Контакт",
"Contact Sets": "Набори контактів",
"Contact sets": "Набори контактів",
"Continue on GitHub": "Продовжити на GitHub",
"Continuity": "Неперервність",
"Control Radius": "Керівний радіус",
"Copy Error Message": "Копіювати повідомлення про помилку",
"Copy Warning Message": "Копіювати попереджувальне повідомлення",
"Copy all licences": "Копіювати всі ліцензії",
@@ -94,8 +115,11 @@
"Corner Relief": "Полегшення кута",
"Corner Relief Shape": "Форма полегшення кута",
"Counterbore depth": "Глибина циліндричного розширення",
"Counterbore Ø": "Ø циліндричного розширення",
"Countersink angle": "Кут зенкування",
"Countersink Ø": "Ø зенкування",
"Create Issue — review draft": "Створити повідомлення про проблему — перегляд чернетки",
"Create Issue…": "Створити повідомлення про проблему…",
"Cross curves": "Поперечні криві",
"Cube": "Куб",
"Current orthographic view (1:1 mm)": "Поточний ортографічний вид (1:1 mm)",
@@ -103,18 +127,28 @@
"Curv. Comb": "Гребінь кривини",
"Curvature (G2)": "Кривина (G2)",
"Curvature change (G3)": "Зміна кривини (G3)",
"Custom": "Власний",
"Customize Toolbar": "Налаштувати панель інструментів",
"Customize…": "Налаштувати…",
"Cylindrical face": "Циліндрична грань",
"Delete": "Видалити",
"Delete Selected Features…": "Видалити вибрані операції…",
"Delete Selected Folders": "Видалити вибрані теки",
"Delete all": "Видалити все",
"Delete rule": "Видалити правило",
"Delete the row": "Видалити рядок",
"Delete the set": "Видалити набір",
"Density": "Густина",
"Dependents": "Залежні",
"Describe the problem you saw": "Опишіть проблему, яку ви побачили",
"Describe what happened before continuing.": "Опишіть, що сталося, перш ніж продовжити.",
"Designs you open or save appear here.": "Проєкти, які ви відкриваєте чи зберігаєте, з'являються тут.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Визначається об'єктом призначення. Виберіть «Кут», щоб редагувати кутову протяжність.",
"Diameter": "Діаметр",
"Direction": "Напрямок",
"Discard": "Відхилити",
"Distance": "Відстань",
"Distance 1": "Відстань 1",
"Do not export hidden edges": "Не експортувати приховані ребра",
"Don't ask again": "Більше не питати",
"Double-click to rename": "Двічі клацніть, щоб перейменувати",
@@ -126,17 +160,22 @@
"Edit Profile Sketch": "Редагувати ескіз профілю",
"Enable rule": "Увімкнути правило",
"Enabled": "Увімкнено",
"End Radius": "Кінцевий радіус",
"End bulge": "Роздуття кінця",
"End continuity": "Неперервність кінця",
"Expand": "Розгорнути",
"Expected result (optional)": "Очікуваний результат (необов'язково)",
"Explode": "Рознести",
"Explode Text": "Розібрати текст",
"Export Animation…": "Експортувати анімацію…",
"Export Bend Table…": "Експортувати таблицю згинів…",
"Export CSV…": "Експорт CSV…",
"Export Current View as DXF": "Експортувати поточний вид як DXF",
"Export Current View as DXF…": "Експортувати поточний вид як DXF…",
"Export DXF": "Експорт DXF",
"Export DXF…": "Експорт DXF…",
"Export JSON…": "Експорт JSON…",
"Export design library...": "Експортувати бібліотеку проєкту...",
"Export hidden edges as dashed lines": "Експортувати приховані ребра як штрихові лінії",
"Expression": "Вираз",
"Extend to meet": "Подовжити до перетину",
@@ -144,31 +183,41 @@
"FILES": "ФАЙЛИ",
"FLAT": "РОЗГОРТКА",
"Face": "Грань",
"Face Appearance": "Вигляд грані",
"Face attachment lost": "Втрачено прив'язку до грані",
"Faces": "Грані",
"Faces / edges / vertices": "Грані / ребра / вершини",
"Faces to split": "Грані для розділення",
"Fastener": "Кріплення",
"Favorites": "Обране",
"Favourite": "Обране",
"Feature": "Операція",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Файли з багатьох CAD-програм мають напрямок Y вгору, тому тут вони відкриваються лежачи на спині (Z вгору).",
"Filter": "Фільтр",
"Filter…": "Фільтр…",
"Find in Browser": "Знайти в браузері",
"Finish": "Оздоблення",
"First plane": "Перша площина",
"Fit and G2 corners require Constant radius.": "Для підгонки та кутів G2 потрібен тип «Constant radius».",
"Fixed Face": "Фіксована грань",
"Flat Pattern": "Розгортка",
"Flat size": "Розмір розгортки",
"Follow camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
"Follow the camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
"Font": "Шрифт",
"From": "Від",
"From Bodies": "З тіл",
"From edges": "Початкові ребра",
"Full Length": "Повна довжина",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "Кути G2 потребують постійного радіуса та підгонки «Exact». Побудова кутів G2 ще не підтримується.",
"Gap": "Зазор",
"Grid": "Сітка",
"HISTORY": "ІСТОРІЯ",
"Height": "Висота",
"Hidden edges": "Приховані ребра",
"History layout": "Розташування історії",
"History names": "Назви в історії",
"Hole (optional)": "Отвір (необов'язково)",
"Holes": "Отвори",
"Holes (edges on them)": "Отвори (ребра на них)",
"Home view": "Головний вид",
@@ -179,14 +228,20 @@
"In place (surfaces)": "На місці (поверхні)",
"In plane": "У площині",
"Inside": "Усередину",
"Instant": "Миттєво",
"Interface font": "Шрифт інтерфейсу",
"Interferes": "Перетинається",
"Invert": "Інвертувати",
"Isocurve": "Ізокрива",
"Issue title": "Заголовок повідомлення",
"Items": "Елементи",
"JOINTS": "З’ЄДНАННЯ",
"Joint Origin1": "Початок з'єднання1",
"Keep (points on it)": "Залишити (точки на ньому)",
"Keep Tools": "Зберегти інструменти",
"Keep of the other surface": "Залишити від іншої поверхні",
"Key a joint's value at this step": "Вставити ключовий кадр зі значенням з'єднання на цьому кроці",
"Keys": "Ключові кадри",
"LIBRARY": "БІБЛІОТЕКА",
"LINKS": "ПОСИЛАННЯ",
"Later": "Пізніше",
@@ -197,9 +252,14 @@
"Letter Spacing": "Міжлітерний інтервал",
"Library": "Бібліотека",
"Library file": "Файл бібліотеки",
"Limits": "Межі",
"Line Spacing": "Міжрядковий інтервал",
"Line Type": "Тип лінії",
"Listing the installed fonts…": "Пошук встановлених шрифтів…",
"Load library": "Завантажити бібліотеку",
"Local library": "Локальна бібліотека",
"Loop": "Контур",
"M5 or M5x0.5": "M5 або M5x0.5",
"MANAGE": "КЕРУВАННЯ",
"MATERIALS": "МАТЕРІАЛИ",
"MODEL": "МОДЕЛЬ",
@@ -210,14 +270,20 @@
"Matched restrictions check the final values.": "Обмеження, умови яких виконано, перевіряють кінцеві значення.",
"Material": "Матеріал",
"Max": "Макс.",
"Maximum": "Максимум",
"Mesh body": "Тіло сітки",
"Mid Radius": "Середній радіус",
"Middle double-click": "Подвійне клацання середньою кнопкою",
"Min": "Мін.",
"Minimum": "Мінімум",
"Mirror Plane": "Площина дзеркала",
"Mitre": "Скіс",
"Model Parameters": "Параметри моделі",
"Modeled": "Змодельовано",
"More toolbar panels": "Більше панелей інструментів",
"Motion": "Рух",
"Mouse gestures": "Жести мишею",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Жести мишею працюють лише в режимі миші SolidWorks (колесо обертає, Ctrl + колесо панорамує).",
"Move / Copy": "Перемістити / копіювати",
"Move Type": "Тип переміщення",
"Move down": "Перемістити вниз",
@@ -235,15 +301,22 @@
"No matching appearance presets.": "Немає відповідних наборів вигляду.",
"No matching appearances": "Немає відповідних виглядів",
"No visible bodies or sketches to export.": "Немає видимих тіл або ескізів для експорту.",
"Nominal diameter": "Номінальний діаметр",
"None": "Немає",
"Not available yet": "Ще недоступно",
"Notch": "Виріз",
"OUTPUT": "ВИВЕДЕННЯ",
"Of": "Ступінь вільності",
"On your computer": "На цьому комп'ютері",
"Opacity": "Непрозорість",
"Open GitHub draft": "Відкрити чернетку GitHub",
"Open library...": "Відкрити бібліотеку...",
"Open local libraries in the desktop app. You can export this design's library here.": "Відкривайте локальні бібліотеки в настільному застосунку. Тут можна експортувати бібліотеку цього проєкту.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Параметричний 3D-CAD з відкритим вихідним кодом на Rust. Ліцензовано за MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Відкриває браузер і копіює текст звіту, щоб ви вставили його на GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Відкриває браузер. Перегляньте й надішліть звіт на GitHub.",
"Operator": "Оператор",
"Orbit (right drag)": "Обертання (перетягування правою кнопкою)",
"Other side": "Інший бік",
"Other surface (mutual trim)": "Інша поверхня (взаємне обрізання)",
"Outside": "Назовні",
@@ -258,8 +331,11 @@
"Pick the tool bodies": "Виберіть тіла інструмента",
"Picked": "Вибрано",
"Picked point": "Вибрана точка",
"Pitch": "Крок",
"Pivot": "Точка обертання",
"Place at the origin instead": "Натомість розмістити в початку координат",
"Plastic Rules": "Правила пластику",
"Play the joint's motion in the view": "Відтворити рух з'єднання у виді",
"Playback": "Відтворення",
"Plot": "Графік",
"Position": "Положення",
@@ -267,14 +343,18 @@
"Position (G0)": "Положення (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Положення вздовж ребра: 0 на початку, 1 на іншому кінці",
"Preferences › Design turns the offer back on": "Налаштування › Дизайн повертає пропозицію",
"Preview": "Попередній перегляд",
"Preview Row": "Рядок перегляду",
"Preview leaves the current design unchanged.": "Попередній перегляд не змінює поточний проєкт.",
"Profile / Edges": "Профіль / ребра",
"Profiles or curves": "Профілі або криві",
"Profiles/Curves": "Профілі/криві",
"Quantity": "Величина",
"RECENT": "НЕДАВНІ",
"REFERENCE": "ДОПОМІЖНА ГЕОМЕТРІЯ",
"RULED SURFACE": "ЛІНІЙЧАСТА ПОВЕРХНЯ",
"RULES": "ПРАВИЛА",
"Radius": "Радіус",
"Radius Fit": "Підгонка за радіусом",
"Ratio": "Співвідношення",
"Reattach selected face": "Повторно приєднати вибрану грань",
@@ -283,6 +363,8 @@
"Rectangular": "Прямокутний",
"Reference face": "Опорна грань",
"Remove Tools": "Прибрати інструменти",
"Remove condition": "Прибрати умову",
"Remove from Folder": "Прибрати з теки",
"Remove from list": "Прибрати зі списку",
"Remove override": "Прибрати перевизначення",
"Remove private design details or paths before sharing.": "Перед публікацією приберіть приватні дані проєкту або шляхи.",
@@ -290,12 +372,18 @@
"Remove the column": "Прибрати стовпець",
"Remove the track": "Прибрати доріжку",
"Remove this point": "Прибрати цю точку",
"Rename Feature": "Перейменувати операцію",
"Rename Folder": "Перейменувати теку",
"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 і надішліть звіт там.",
"Reports are public. A GitHub account is required.": "Звіти загальнодоступні. Потрібен обліковий запис GitHub.",
"Reset Toolbar": "Скинути панель інструментів",
"Resize Part": "Змінити розмір деталі",
"Restore View": "Відновити вид",
"Restore outer boundary": "Відновити зовнішню межу",
"Restore the inherited component or physical material appearance": "Відновити успадкований вигляд компонента або фізичного матеріалу",
"Restore this studio's tools": "Відновити інструменти цієї студії",
"Result": "Результат",
"Retake screenshot": "Повторити знімок екрана",
"Reverse": "Обернути",
@@ -305,7 +393,14 @@
"Revert Pos.": "Скасув. полож.",
"Revert Position": "Повернути положення",
"Review the report and screenshot before opening GitHub.": "Перегляньте звіт і знімок екрана перед відкриттям GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Перегляньте звіт на GitHub і натисніть Submit new issue, щоб опублікувати його.",
"Rib Length": "Довжина ребра",
"Rib Offset": "Зсув ребра",
"Rib Thickness": "Товщина ребра",
"Ribs": "Ребра",
"Rim Edge": "Крайове ребро",
"Ring of tools on the right button": "Кільце інструментів на правій кнопці",
"Roll Back Here": "Відкотити сюди",
"Roll History Marker Here": "Перемістити маркер історії сюди",
"Round": "Круглий",
"Rule": "Правило",
@@ -315,21 +410,28 @@
"SURFACE QUALITY": "ЯКІСТЬ ПОВЕРХНІ",
"Save Rule": "Зберегти правило",
"Save View": "Зберегти вид",
"Save screenshot…": "Зберегти знімок екрана…",
"Save the study as an animated GIF, seen as the viewport shows it": "Зберегти дослідження як анімований GIF, як його показує вікно перегляду",
"Saved preview. Geometry is being checked before editing.": "Збережений перегляд. Геометрію перевіряють перед редагуванням.",
"Scale factor": "Коефіцієнт масштабу",
"Scene": "Сцена",
"Screenshot (optional)": "Знімок екрана (необов'язково)",
"Search": "Пошук",
"Search commands": "Пошук команд",
"Search this library": "Пошук у цій бібліотеці",
"Second plane": "Друга площина",
"Section": "Переріз",
"Section position": "Положення перерізу",
"Sections (one sketch each)": "Перерізи (по одному ескізу)",
"Select Other": "Вибрати інше",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Вибір іншого: утримуйте ліву кнопку нерухомо на моделі (або клацніть правою › Вибрати інше…), щоб побачити список усього під курсором; клацніть ще раз у тому ж місці, щоб вибрати наступний об'єкт там. Двічі клацніть середньою кнопкою, щоб перейти до головного виду (Налаштування › Навігація).",
"Select bodies (a face picks its body)": "Виділяти тіла (грань виділяє своє тіло)",
"Select bodies or faces instead": "Натомість виділіть тіла або грані",
"Select edges": "Виберіть ребра",
"Selection filter": "Фільтр виділення",
"Semantic faces share their appearance across instances.": "Семантичні грані спільно використовують свій вигляд між екземплярами.",
"Send your report on GitHub": "Надіслати звіт на GitHub",
"Shaded": "Затінений",
"Shape": "Форма",
"Sheet Rules": "Правила листа",
"Show Agent Cursor": "Показати курсор агента",
@@ -348,6 +450,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрився до того, як ці проєкти було збережено. Можна відкрити їхнє останнє автозбереження.",
"Specialise": "Спеціалізувати",
"Speed": "Швидкість",
"Splitting tool": "Інструмент розділення",
"Square": "Квадрат",
"Stand it up": "Вирівняти",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Вирівняти: поворот на чверть оберту навколо X як одна операція переміщення, яку можна скасувати.",
@@ -357,17 +460,27 @@
"Start continuity": "Неперервність початку",
"Step": "Крок",
"Steps": "Кроки",
"Steps to reproduce (optional)": "Кроки відтворення (необов'язково)",
"Stitch the kept pieces": "Зшити залишені частини",
"Stitch to both bodies": "Зшити з обома тілами",
"Studio": "Студія",
"Study": "Дослідження",
"Style": "Стиль",
"Summary": "Короткий опис",
"Suppress": "Придушити",
"Suppressed": "Придушено",
"Surface": "Поверхня",
"Surface, or faces to turn over": "Поверхня або грані, щоб обернути нормаль",
"Swap Ends": "Поміняти кінці місцями",
"TRIM": "ОБРІЗАННЯ",
"Tangent (G1)": "Дотик (G1)",
"Tangent Chain": "Дотичний ланцюг",
"Tangent chain": "Дотичний ланцюг",
"Tap drill diameter": "Діаметр свердла під різьбу",
"Target": "Цільова грань або площина",
"Target Body": "Цільове тіло",
"Tear": "Розрив",
"Technical details": "Технічні подробиці",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Штриховий ескіз — це збережений орієнтир площини. Виберіть нову грань, щоб відновити прив'язку.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Збережений вихідний екземпляр відсутній. Перед повторною прив'язкою виберіть дійсне вихідне розміщення.",
"Then": "Потім",
@@ -379,24 +492,32 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Цей дочірній об'єкт спільний. Його видалення прибирає його з кожної копії батьківського.",
"This child is shared. Position changes apply to every copy of its parent.": "Цей дочірній об'єкт спільний. Зміни положення застосовуються до кожної копії батьківського.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ця грань не може забезпечити надійну прив'язку. Виберіть іншу грань або явно використайте фіксовану площину.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Це зображення ще не додано до GitHub. Скопіюйте або збережіть його, а потім додайте на GitHub.",
"To": "До",
"To Offset": "До зсуву",
"To edges": "Кінцеві ребра",
"Tolerance": "Допуск",
"Too close": "Занадто близько",
"Tool Bodies": "Тіла інструмента",
"Tools": "Інструменти",
"Tools, clockwise from the top": "Інструменти за годинниковою стрілкою згори",
"Trimming tool (plane, face or curves)": "Інструмент обрізання (площина, грань або криві)",
"Turn off section analysis before exporting the view.": "Вимкніть аналіз перерізу перед експортом виду.",
"Type": "Тип",
"UNTRIM": "ЗНЯТИ ОБРІЗАННЯ",
"Unit": "Одиниця",
"Unsuppress": "Скасувати придушення",
"Updating the preview": "Оновлення попереднього перегляду",
"Use active author placement": "Використати активне вихідне розміщення",
"Use fixed plane": "Використати фіксовану площину",
"Use saved fixed plane": "Використати збережену фіксовану площину",
"Use selected face as fixed plane": "Використати вибрану грань як фіксовану площину",
"Use the SolidWorks mouse mode": "Використовувати режим миші SolidWorks",
"User Parameters": "Параметри користувача",
"VIEW": "ВИД",
"VIEWS": "ВИГЛЯДИ",
"Value": "Значення",
"Variable Radius": "Змінний радіус",
"Velocity": "Швидкість",
"Vertices": "Вершини",
"View navigator": "Навігатор виду",
@@ -404,16 +525,50 @@
"Wall": "Стінка",
"Wall Location": "Розташування стінки",
"Wall Thickness": "Товщина стінки",
"What did you do just before the problem? A short list is enough": "Що ви робили безпосередньо перед проблемою? Короткого переліку достатньо",
"What did you expect to see?": "Що ви очікували побачити?",
"What happened?": "Що сталося?",
"When": "Коли",
"Width": "Ширина",
"Will fail": "Зазнає невдачі",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Коли жести ввімкнено, права кнопка більше не обертає вид: утримуйте її та рухайте, щоб відкрити кільце, а потім наведіть на зону інструмента. Звичайне клацання правою кнопкою так само відкриває меню.",
"X": "X",
"X Angle": "Кут X",
"X angle": "Кут X",
"X distance": "Відстань X",
"Y": "Y",
"Y Angle": "Кут Y",
"Y angle": "Кут Y",
"Y distance": "Відстань Y",
"Z": "Z",
"Z Angle": "Кут Z",
"Z Distance": "Відстань Z",
"Z angle": "Кут Z",
"Z distance": "Відстань Z",
"Zebra": "Зебра",
"ai.prefs.availability": "Керування асистентом",
"ai.prefs.background_commands": "Виконувати команди агента у фоновому режимі",
"ai.prefs.follow_camera": "Повертати камеру до ескізів",
"ai.prefs.follow_camera_off": "Автоматичне стеження камери вимкнено.",
"ai.prefs.follow_camera_on": "Камера повертається до ескізів, які створює агент.",
"ai.prefs.intro": "Агенти, дозволи та те, як показується їхня робота.",
"ai.prefs.make_default": "Використовувати за замовчуванням",
"ai.prefs.no_agents": "Агентів ще немає. Додайте агента або оновіть список, щоб знайти встановлених.",
"ai.prefs.no_saved_approvals": "Немає збережених дозволів.",
"ai.prefs.override_all_warning": "Цей агент може зберігати, перезаписувати, експортувати або замінювати проєкт без запиту.",
"ai.prefs.permission_all": "Дозволяти все",
"ai.prefs.permission_all_summary": "Доступні інструменти SolidCraft працюють без запиту.",
"ai.prefs.permission_ask": "Запитувати перед змінами",
"ai.prefs.permission_ask_summary": "Підтверджувати кожну зміну проєкту та дію з файлами.",
"ai.prefs.permission_edits": "Дозволяти редагування моделі",
"ai.prefs.permission_edits_summary": "Редагування моделі виконується; для дій з файлами й руйнівних дій усе ще запитують дозвіл.",
"ai.prefs.permission_scope": "Лише для інструментів SolidCraft. Оболонка агента та редагування файлів підкоряються його власним дозволам.",
"ai.prefs.pointer_preview_hidden": "Вказівник приховано",
"ai.prefs.pointer_preview_speed": "Вказівник агента · {speed}",
"ai.prefs.pointer_speed": "Швидкість руху",
"ai.prefs.privacy_note": "Розмову отримує лише вибраний вами агент або кінцева точка. Ключі кінцевих точок зберігаються в системному сховищі ключів.",
"ai.prefs.show_pointer": "Показати вказівник агента",
"ai.prefs.turn_limit_hint": "Максимальна кількість викликів інструментів, що змінюють проєкт, які Асистент може зробити за одне повідомлення.",
"angle, height": "кут, висота",
"appearance.dielectric_ior": "Діелектричний IOR",
"appearance.metallic": "Металевість",
@@ -770,6 +925,7 @@
"cmd.view_graphics_preset": "Набір графіки",
"cmd.view_list": "Список іменованих видів",
"cmd.view_navigate": "Навігація видом",
"cmd.view_normal_to": "Перпендикулярно до виділення",
"cmd.view_rename": "Перейменувати іменований вид",
"cmd.view_save": "Новий іменований вид",
"color.copy": "Клацніть, щоб скопіювати значення кольору",
@@ -867,6 +1023,12 @@
"help.licences": "Ліцензії",
"help.mit": "Ліцензія MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Зрізає вибрані ребра плоским скосом на заданій відстані. Використовуйте, щоб притупити гострі ребра або полегшити вставляння штифта чи гвинта.",
"help.solid.extrude": "Витягує профіль ескізу в 3D на задану відстань, додаючи або видаляючи матеріал. Використовуйте, щоб перетворити плоский контур на брусок, бобишку або кишеню.",
"help.solid.fillet": "Закруглює вибрані ребра плавним круговим переходом заданого радіуса. Використовуйте, щоб пом'якшити гострі ребра для міцності, зручності чи вигляду.",
"help.solid.hole": "Просвердлює круглий отвір заданого діаметра в обраній точці на грані. Використовуйте для отворів під гвинти, штифтів та інших простих круглих отворів.",
"help.solid.revolve": "Обертає профіль ескізу навколо осі, утворюючи кругле тіло. Використовуйте для валів, ручок, чаш та будь-яких деталей, круглих навколо осьової лінії.",
"help.solid.shell": "Робить тіло порожнистим, залишаючи стінки заданої товщини, і може відкривати вибрані грані. Використовуйте для коробок, кришок та інших тонкостінних деталей.",
"help.third_party": "Ліцензії сторонніх компонентів",
"home.back": "Назад до {name}",
"home.crashes": "Звіти про збої",
@@ -976,6 +1138,15 @@
"motion.to_end": "У кінець",
"motion.to_start": "На початок",
"motion.track_name": "Доріжка {number}",
"nav.alias.help": "Shift+Alt+ліва обертає, Shift+Alt+середня панорамує, Shift+Alt+права масштабує; середня також панорамує, права обертає",
"nav.blender.help": "Середня обертає, Shift+середня панорамує, Ctrl+середня масштабує; права не рухає камеру",
"nav.free_orbit.help": "Миша та 3D-миша обертають через обидва полюси й зберігають крен. Прив'язування до осей через Shift вимкнено.",
"nav.free_orbit.label": "Вільне обертання",
"nav.fusion.help": "Середня панорамує, Shift+середня обертає, права обертає",
"nav.inventor.help": "Середня панорамує, Shift+середня обертає, Ctrl+середня масштабує, права обертає",
"nav.openscad.help": "Ліва обертає по порожньому місцю, Shift+ліва виділяє рамкою, геометрію ескізу перетягують для редагування; права або середня панорамує, Shift+права або Shift+середня масштабує",
"nav.solidworks.help": "Середня обертає, Ctrl+середня панорамує, Shift+середня масштабує, права обертає",
"nav.tinkercad.help": "Права обертає, середня або Shift+права панорамує",
"nav.touchpad.help": "Утримуйте Shift і рухайте вказівник, щоб панорамувати; утримуйте Alt/Option і рухайте, щоб обертати. Кнопка миші не потрібна. Вертикальна прокрутка масштабує.",
"nav.touchpad.label": "Трекпад",
"nav.touchpad.orbit": "Обертання (утримуйте Alt/Option і рухайте; без кнопки миші)",
@@ -998,8 +1169,130 @@
"pattern.flip_direction": "Змінити напрямок",
"pick a hole to seat it, or OK places it at the origin": "виберіть отвір для посадки, або OK розмістить його в початку координат",
"pick one or two vertices, edges or faces": "виберіть одну-дві вершини, ребра або грані",
"prefs.add_item": "Додати елемент",
"prefs.alt_frees_manipulators": "Alt / Option також дає вільне перетягування стрілок маніпулятора",
"prefs.anti_aliasing_note": "Згладжує нерівні краї.",
"prefs.applies_on_restart": "Застосовується після перезапуску",
"prefs.assistant": "Асистент",
"prefs.axes": "Осі",
"prefs.axis": "Вісь",
"prefs.bottom": "Низ",
"prefs.browser": "Браузер",
"prefs.built_in": "Вбудована",
"prefs.button_left": "Перетягування лівою кнопкою",
"prefs.button_middle": "Перетягування середньою кнопкою",
"prefs.button_right": "Перетягування правою кнопкою",
"prefs.choose_font": "Вибрати файл шрифту…",
"prefs.click": "Клацання",
"prefs.contact_shading": "М'яке контактне затінення",
"prefs.contact_shading_note": "М'яке затінення там, де деталі зближуються.",
"prefs.context_menu": "Контекстне меню",
"prefs.context_menu_ring": "Контекстне меню · утримуйте для кругового меню",
"prefs.cpu": "ЦП (програмний)",
"prefs.crash_reports": "Відкрити звіти про збої…",
"prefs.crash_reports_note": "Звіти зберігаються на цьому комп'ютері разом із проєктом і командами, що відтворюють проблему.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Криві поверхні",
"prefs.curves": "Криві поверхні",
"prefs.dead_zone_note": "Заштрихована смуга — це мертва зона: рух у її межах ігнорується.",
"prefs.default": "типово",
"prefs.design": "Типові параметри проєкту",
"prefs.design_note": "З чого починаються нові проєкти. Відкриті проєкти зберігають власні параметри.",
"prefs.devices": "3D-миша",
"prefs.display": "Відображення",
"prefs.edges": "Ребра та затінення",
"prefs.extend": "Розширення",
"prefs.extra_navigation": "Ctrl+перетягування лівою на порожньому місці обертає; утримуйте Space, щоб панорамувати.",
"prefs.follows_os": "Як у системі",
"prefs.font": "Шрифт",
"prefs.font_fallback": "Символи, яких немає у вибраному шрифті, беруться з вбудованого, тож жоден напис не показує порожній квадрат.",
"prefs.font_sample": "Ґедзь, що їв фінік, з'їв і юшку · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Не вдалося прочитати вибраний шрифт.",
"prefs.general": "Загальні",
"prefs.general_note": "Мова, збереження та оновлення.",
"prefs.gesture_orbit": "Обертання",
"prefs.gesture_pan": "Панорамування",
"prefs.gesture_zoom": "Масштаб",
"prefs.graphics": "Графіка",
"prefs.graphics_note": "Обміняйте швидкість на плавність. Кожен варіант показує свій ефект на зразку.",
"prefs.graphics_presets_note": "Набори задають наведені нижче параметри разом.",
"prefs.history": "Історія",
"prefs.hold_stop_snapping": "Утримуйте, щоб вимкнути прив'язування",
"prefs.icons_only": "Лише піктограми",
"prefs.imported_file": "Імпортований файл",
"prefs.input": "Введення",
"prefs.interface": "Інтерфейс",
"prefs.interface_language": "Мова інтерфейсу",
"prefs.interface_note": "Як виглядає SolidCraft і де розташовані його частини.",
"prefs.interval_minutes": "{n} хв",
"prefs.labels": "Підписи",
"prefs.layout": "Компонування",
"prefs.layout_note": "«Компактне» прикріплює панелі до країв вікна. Прозорість браузера доступна в режимі «Плаваюче». Сторону Асистента можна змінити на сторінці «Асистент».",
"prefs.left": "Ліворуч",
"prefs.left_click": "Клацання лівою",
"prefs.live_input": "Поточне введення",
"prefs.live_preview": "Попередній перегляд у реальному часі",
"prefs.minutes": "хв",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Миша й трекпад",
"prefs.mouse_heading": "Миша",
"prefs.mouse_mapping": "Призначення кнопок миші",
"prefs.navigation": "Навігація",
"prefs.navigation_note": "Як миша, трекпад і 3D-миша рухають вид.",
"prefs.not_on_gpu": "не на цьому GPU",
"prefs.on": "Увімкнено",
"prefs.opaque": "Непрозорий",
"prefs.orbit_snap": "Утримуйте Shift під час обертання, щоб прив'язатися до осі.",
"prefs.orientation": "Орієнтація",
"prefs.overall_speed": "Загальна швидкість",
"prefs.preview_choices": "Перегляд ваших виборів",
"prefs.quality_default": "типово",
"prefs.quality_fastest": "найшвидше",
"prefs.quality_slowest": "найповільніше",
"prefs.quality_smoother": "плавніше",
"prefs.reports_saved": "Збережено: {n}",
"prefs.right_click": "Клацання правою",
"prefs.saving": "Автозбереження й відновлення",
"prefs.scale_note": "Також Ctrl + / Ctrl − у вікні. Кнопки мають 28 px за 100%.",
"prefs.scale_text": "Масштаб і текст",
"prefs.select": "Вибрати",
"prefs.select_one_item": "Вибрати один елемент",
"prefs.selecting": "Вибір",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift лише додає; Ctrl / Cmd перемикає",
"prefs.shift_add_note": "Увімкнено: Shift додає, Ctrl / Cmd перемикає. Вимкнено (типово): обидва перемикають.",
"prefs.shortcuts": "Комбінації клавіш",
"prefs.sketch_selection": "Ескіз і вибір",
"prefs.sketch_selection_heading": "Ескіз і вибір",
"prefs.sketch_selection_note": "Як ескізи прив'язуються й як клацання вибирають.",
"prefs.sketching": "Ескізування",
"prefs.snap_drawing_note": "Під час малювання або перетягування точки ескізу.",
"prefs.source": "Джерело",
"prefs.speed": "Швидкість",
"prefs.stand_up_note": "Коли файл STEP або IGES відкрито з віссю Z угору, пропонувати поворот на чверть оберту навколо X.",
"prefs.status_done": "Готово",
"prefs.status_error": "Помилка",
"prefs.status_selected": "Вибрано",
"prefs.status_warning": "Попередження",
"prefs.supersampling_brief": "Малює вид у більшому розмірі й зменшує його: дуже чітко, але потребує більше ресурсів.",
"prefs.tab_accent_note": "Акцентний колір відповідає активній вкладці",
"prefs.theme_file": "файл теми",
"prefs.timeline_half_hour": "−30 хв",
"prefs.timeline_now": "зараз",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Додати або прибрати елемент",
"prefs.toolbar": "Панель інструментів",
"prefs.translucent": "Напівпрозорий",
"prefs.under_browser": "Під браузером",
"prefs.under_cursor": "Під курсором",
"prefs.units_precision": "Одиниці й точність",
"prefs.updates": "Оновлення",
"prefs.view": "Поведінка виду",
"prefs.viewport": "Вікно перегляду",
"prefs.wheel": "Колесо",
"prefs.wheel_trackpad": "Колесо й трекпад",
"prefs.workspace": "Робоча область",
"prefs.zoom_toward_cursor": "Масштаб до курсора",
"properties.title": "Властивості: {name}",
"recovery.days_ago": "{count} дн. тому",
"recovery.detail": "{file} · елементів: {features} · автозбережено {when}",
@@ -1361,6 +1654,8 @@
"ui.agent_chat_waiting": "Очікує на вашу відповідь",
"ui.agent_cursor": "Курсор агента",
"ui.agent_cursor_speed": "Швидкість курсора агента",
"ui.agent_off_thread": "Підтримувати чуйність вікна, поки агенти виконують команди",
"ui.agent_off_thread_note": "Повільні команди агента (з панелі ШІ або клієнта MCP) обчислюються паралельно з вікном: ви можете далі обертати й оглядати вид і зупинити команду з індикатора у виді. Вимкнено: кожна виконується в потоці вікна, і вікно чекає на неї.",
"ui.ai_add": "Додати",
"ui.ai_add_agent": "Додати агента…",
"ui.ai_add_agent_title": "Додати агента",
@@ -1592,6 +1887,8 @@
"ui.filter_commands_or_keys": "Фільтрувати команди або клавіші",
"ui.findInTimeline": "Знайти в часовому ланцюжку",
"ui.findInWindow": "Знайти у вікні",
"ui.fine": "Висока",
"ui.finest": "Максимальна",
"ui.finish_sketch": "Завершити ескіз",
"ui.fit": "Вписати",
"ui.fix_unfix": "Закріпити/розкріпити",
@@ -1689,10 +1986,17 @@
"ui.hideProfile": "Приховати профіль",
"ui.history_back": "На один крок назад",
"ui.history_below_browser": "Під браузером",
"ui.history_edge_hint": "двічі клацніть край",
"ui.history_first": "Перейти на початок",
"ui.history_forward": "На один крок уперед",
"ui.history_hide_names": "Приховати назви",
"ui.history_last": "Перейти в кінець",
"ui.history_marker_tip": "Маркер історії: перетягніть, щоб відкотити модель назад; нові елементи додаються тут",
"ui.history_names": "Назви",
"ui.history_play": "Відтворити до кінця",
"ui.history_show_names": "Показати назви",
"ui.history_title": "Історія",
"ui.history_toggle_names": "Показувати або приховувати назви елементів",
"ui.hole": "Отвір",
"ui.hole_tap_type": "Тип різьби отвору",
"ui.hole_type": "Тип отвору",
@@ -1816,6 +2120,7 @@
"ui.project_face_edges_sketch": "Проєктувати ребра граней у нові ескізи",
"ui.properties": "Властивості",
"ui.pull_direction": "Напрямок витягування",
"ui.quality": "Якість",
"ui.quantity": "Кількість",
"ui.quantity_2": "Кількість 2",
"ui.quit": "Вихід",
@@ -1910,6 +2215,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Розділити тіло",
"ui.stand_up_tip": "Після відкриття файлу STEP або IGES з напрямком Z вгору пропонувати поворот на чверть оберту навколо X",
"ui.standard": "Стандартна",
"ui.start": "Початок",
"ui.start_over": "Почати спочатку",
"ui.start_page": "Початкова сторінка",
+4
View File
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Lọc lệnh hoặc phím tắt",
"ui.findInTimeline": "Tìm trong lịch sử thiết kế",
"ui.findInWindow": "Tìm trong cửa sổ",
"ui.fine": "Mịn",
"ui.finest": "Mịn nhất",
"ui.finish_sketch": "Hoàn tất phác thảo",
"ui.fit": "Vừa vặn (Fit)",
"ui.fix_unfix": "Cố định/Uncố định",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Chiếu các cạnh bề mặt vào phác thảo mới",
"ui.properties": "Thuộc tính cơ lý",
"ui.pull_direction": "Hướng kéo",
"ui.quality": "Chất lượng",
"ui.quantity": "Số lượng",
"ui.quantity_2": "Số lượng 2",
"ui.quit": "Thoát ứng dụng",
@@ -1910,6 +1913,7 @@
"ui.spline": "Đường cong Spline",
"ui.split_body": "Chia tách khối",
"ui.stand_up_tip": "Sau khi mở tệp STEP hoặc IGES với trục Z hướng lên, đề xuất xoay 90° quanh trục X",
"ui.standard": "Tiêu chuẩn",
"ui.start": "Bắt đầu",
"ui.start_over": "Bắt đầu lại",
"ui.start_page": "Trang bắt đầu",
+4
View File
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "篩選命令或按鍵",
"ui.findInTimeline": "在時間軸中查詢",
"ui.findInWindow": "在視窗中查詢",
"ui.fine": "精細",
"ui.finest": "最精細",
"ui.finish_sketch": "完成草圖",
"ui.fit": "適合",
"ui.fix_unfix": "固定/取消固定",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "將面的邊投影到新草圖",
"ui.properties": "屬性",
"ui.pull_direction": "拔模方向",
"ui.quality": "品質",
"ui.quantity": "數量",
"ui.quantity_2": "數量 2",
"ui.quit": "結束",
@@ -1852,6 +1855,7 @@
"ui.spline": "雲形線",
"ui.split_body": "分割實體",
"ui.stand_up_tip": "當 Z 朝上的 STEP 或 IGES 檔案開啟後,提示繞 X 軸旋轉四分之一週",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "重新開始",
"ui.start_page": "起始頁面",
+46 -2
View File
@@ -143,6 +143,7 @@
"Cube": "立方体",
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
"Current perspective view": "当前透视视图",
"Current plane": "当前平面",
"Curv. Comb": "曲率梳",
"Curvature (G2)": "曲率 (G2)",
"Curvature change (G3)": "曲率变化 (G3)",
@@ -239,7 +240,9 @@
"Gap": "间隙",
"Grid": "网格",
"HISTORY": "历史",
"Halo under sketch curves": "在草图曲线下方显示光晕",
"Height": "高度",
"Height From": "高度基准",
"Hidden edges": "隐藏线",
"History layout": "历史布局",
"History names": "历史名称",
@@ -256,6 +259,7 @@
"In place (surfaces)": "原位(曲面)",
"In plane": "在平面内",
"Included": "夹角",
"Inner Sharp": "内侧虚拟尖点",
"Inside": "内侧",
"Instant": "即时",
"Interface font": "界面字体",
@@ -302,6 +306,7 @@
"Mass Center": "质心",
"Matched restrictions check the final values.": "满足条件的限制规则检查最终值。",
"Material": "材质",
"Material Outside": "材料在外",
"Material copied. Existing body assignments are unchanged.": "材质已复制。现有实体指定保持不变。",
"Max": "最大",
"Maximum": "最大",
@@ -314,7 +319,6 @@
"Mitre": "斜接",
"Model Parameters": "模型参数",
"Modeled": "已建模",
"More themes": "更多主题",
"More toolbar panels": "更多工具栏面板",
"Motion": "运动",
"Mouse gestures": "鼠标手势",
@@ -349,8 +353,10 @@
"On your computer": "在此计算机上",
"Opacity": "不透明度",
"Open GitHub draft": "打开 GitHub 草稿",
"Open bodies (no volume)": "未闭合实体(无体积)",
"Open library...": "打开库...",
"Open local libraries in the desktop app. You can export this design's library here.": "请在桌面应用中打开本地库。您可以在此导出当前设计的库。",
"Open surface — no volume": "未闭合曲面 — 无体积",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
"Opens your browser and copies the report text for you to paste into GitHub.": "打开浏览器,并复制报告文本以便粘贴到GitHub。",
"Opens your browser. Review and submit your report on GitHub.": "打开浏览器。请在GitHub中检查并提交报告。",
@@ -359,6 +365,7 @@
"Other side": "另一侧",
"Other surface (mutual trim)": "另一曲面(相互修剪)",
"Other way": "反方向",
"Outer Sharp": "外侧虚拟尖点",
"Outer radius": "外圆角半径",
"Outside": "外侧",
"Overall length": "总长",
@@ -388,7 +395,6 @@
"Preview": "预览",
"Preview Row": "预览行",
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
"Previous themes": "上一组主题",
"Profile / Edges": "轮廓 / 边",
"Profiles or curves": "轮廓或曲线",
"Profiles/Curves": "轮廓/曲线",
@@ -526,9 +532,11 @@
"Surface, or faces to turn over": "曲面,或要翻转的面",
"Swap Ends": "交换端点",
"TRIM": "修剪",
"Tangent": "相切",
"Tangent (G1)": "相切 (G1)",
"Tangent Chain": "相切链",
"Tangent chain": "相切链",
"Tangent to Edge": "与边相切",
"Tap drill diameter": "攻丝底孔直径",
"Target": "目标",
"Target Body": "目标实体",
@@ -1081,6 +1089,7 @@
"crash.report": "报告…",
"crash.unclean_explanation": "在后续启动时检测到;未记录确切的退出时间。系统关机或强制停止也可能导致会话未正常结束。",
"delete.and_more": "…以及另外 {count} 项",
"diagnostic.empty_boolean_component": "组件 '{component}' 中没有可供切除或求交的实体;工具可能与组件 '{other}' 中的 '{body}' 重叠 — 请激活该组件或选择该实体作为目标",
"dialog.bodies_count": "{count} 个实体",
"dialog.clearance_invalid": "间隙必须是 0 或更大的长度",
"dialog.compute": "计算",
@@ -1127,6 +1136,7 @@
"file_load.built": "已构建:{built}/{total} 个特征。",
"file_load.converting": "正在转换实体 {done}/{total}",
"file_load.inserting": "正在插入几何体",
"file_load.link_item": "{name} ({done}/{total})",
"file_load.opening": "正在打开设计",
"file_load.preparing_thumbnail": "正在准备缩略图...",
"file_load.preparing_view": "正在准备视图...",
@@ -1136,6 +1146,7 @@
"file_load.rebuilding": "正在重建特征 {done}/{total}",
"file_load.recovering": "正在恢复设计",
"file_load.stopping": "正在等待上一次操作停止...",
"file_load.updating_links": "正在更新链接",
"file_load.waiting": "正在等待文件操作",
"fits the face, e.g. M8": "适配该面,例如 M8",
"from the profile": "取自型材",
@@ -1238,6 +1249,8 @@
"joint.from_bodies_hint": "每个所选实体移入各自的新组件,位置保持不变。",
"joint.one_body": "两次选择都在 {body} 上。请在另一个零件上选择第二个。",
"joint.one_component": "这些零件是同一组件的实体:{bodies}。接头使一个组件相对另一个组件运动,因此每个零件都需要自己的组件。",
"joint.rigid_group_one_component": "这 {count} 个实体都位于同一组件中(或直接位于设计中):请先使用“{command}”。",
"joint.rigid_group_select_components": "请选择两个或更多组件。",
"keymap.command_id": "命令 ID",
"keymap.empty": "没有匹配的命令",
"keymap.use_anyway": "仍在此处使用 {key}",
@@ -1401,6 +1414,10 @@
"prefs.gesture_pan": "平移",
"prefs.gesture_zoom": "缩放",
"prefs.graphics": "图形",
"prefs.graphics_card": "显卡",
"prefs.graphics_card_auto": "自动",
"prefs.graphics_card_missing": "已保存的显卡不可用;将使用自动选择。",
"prefs.graphics_card_restart": "重启 SolidCraft 后生效。WGPU_ADAPTER_NAME 优先于此选择。",
"prefs.graphics_note": "在速度与平滑度之间取舍。每个选项均以示例展示效果。",
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
"prefs.history": "历史",
@@ -1432,6 +1449,7 @@
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
"prefs.orientation": "方向",
"prefs.overall_speed": "整体速度",
"prefs.prefer_free_sketch_points": "草图点重合时优先选择可移动的点",
"prefs.preview_choices": "所选设置预览",
"prefs.quality_default": "默认",
"prefs.quality_fastest": "最快",
@@ -2138,6 +2156,11 @@
"sketch_panel.author": "创建时的放置:{path}",
"sketch_panel.dimension_hint": "输入数值或表达式,然后按“确定”。",
"sketch_panel.move_hint": "输入距离和角度,然后按“确定”。",
"sketch_panel.plane_angle": "{plane},旋转 {angle}",
"sketch_panel.plane_fixed": "固定平面",
"sketch_panel.plane_flipped": "{plane}(从另一侧)",
"sketch_panel.plane_offset": "{plane},偏移 {distance}",
"sketch_panel.plane_origin": "{plane} 平面",
"sketch_panel.redefine_title": "{command}:{name}",
"sketch_panel.sheet_attachment": "附着到钣金实体的平面上;钣金变化时会跟随该面板。",
"sketch_panel.text_hint": "编辑文字及其位置,然后按“确定”。",
@@ -2312,6 +2335,7 @@
"ui.agent_chat_changes": "{n} 项更改,一步撤销",
"ui.agent_chat_changes_n": "{n} 项更改",
"ui.agent_chat_changes_of": "更改 {n}/{limit}",
"ui.agent_chat_choose_model": "选择模型",
"ui.agent_chat_close": "关闭",
"ui.agent_chat_command": "智能体命令",
"ui.agent_chat_connect": "连接",
@@ -2387,6 +2411,9 @@
"ui.agent_chat_previous_picture": "上一张图片",
"ui.agent_chat_privacy": "除了你以自己的账户运行的智能体之外,不会向任何地方发送任何内容。",
"ui.agent_chat_privacy_note": "SolidCraft 不向任何地方发送任何内容:你运行的智能体以你自己的账户与它自己的服务通信。SolidCraft 从不查看或保存任何密钥或密码。",
"ui.agent_chat_provider_models": "{count} 个模型",
"ui.agent_chat_provider_other": "其他",
"ui.agent_chat_provider_tip": "旁边的列表显示此提供商的模型。",
"ui.agent_chat_queue": "加入队列",
"ui.agent_chat_queue_full": "等待中的消息太多:请删除一条,或等待智能体完成。",
"ui.agent_chat_queued": "已排队",
@@ -2594,6 +2621,8 @@
"ui.copy": "复制",
"ui.corner": "角",
"ui.corners": "角点",
"ui.cosmetic_thread_markings": "装饰螺纹标记",
"ui.cosmetic_thread_markings_help": "在带螺纹的圆柱面上显示示意标记。仅改变显示,不影响设计或导出。",
"ui.counterbore": "沉头孔",
"ui.countersink": "锥形沉孔",
"ui.create": "创建",
@@ -2670,6 +2699,8 @@
"ui.filter_commands_or_keys": "筛选命令或按键",
"ui.findInTimeline": "在时间线中查找",
"ui.findInWindow": "在窗口中查找",
"ui.fine": "精细",
"ui.finest": "最精细",
"ui.finish_sketch": "完成草图",
"ui.fit": "适合",
"ui.fix_unfix": "固定/取消固定",
@@ -2882,6 +2913,11 @@
"ui.paste": "粘贴",
"ui.path": "路径",
"ui.pattern_on_path": "路径阵列",
"ui.perforate_margin_default": "与直径相同",
"ui.perforate_pitch": "孔距",
"ui.perforate_square": "正方形排列",
"ui.perforate_staggered": "交错排列(60°)",
"ui.perforate_title": "穿孔",
"ui.perpendicular": "垂直",
"ui.perspective_view": "透视图",
"ui.pinToShortcuts": "固定到快捷方式",
@@ -2893,7 +2929,9 @@
"ui.plugins": "插件",
"ui.plugins_desktop": "插件仅在桌面版中可用。",
"ui.plugins_empty": "尚未安装插件。",
"ui.plugins_none": "没有符合搜索的插件。",
"ui.plugins_note": "每个插件是 {path} 里的一个文件夹,其中有 plugin.json。SolidCraft 只读取该文件,不运行文件夹里的代码。关闭插件仍会保留安装。",
"ui.plugins_search": "搜索插件",
"ui.plugins_skipped": "有 {count} 个文件夹无法读取。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2909,6 +2947,7 @@
"ui.project_face_edges_sketch": "将面的边投影到新草图",
"ui.properties": "属性",
"ui.pull_direction": "拔模方向",
"ui.quality": "质量",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "退出",
@@ -3015,6 +3054,7 @@
"ui.spline": "样条曲线",
"ui.split_body": "分割实体",
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
"ui.standard": "标准",
"ui.start": "开始",
"ui.start_over": "重新开始",
"ui.start_page": "起始页",
@@ -3124,6 +3164,7 @@
"update.prefs_flatpak": "此副本({version})由 Flatpak 更新:运行 {command} 或使用软件中心。SolidCraft 不会自行检查。",
"update.prefs_label": "更新",
"update.prefs_note": "当前为 SolidCraft {version}({kind})。检查时只向 GitHub 查询最新版本,不发送其他任何数据。",
"update.prefs_web": "重新加载页面以获取最新的网页应用。如果网络不可用或更新无法及时下载完成,则打开已保存的版本;正在进行的下载会继续,供下次重新加载时使用。",
"update.release_page": "打开发布页面",
"update.restart": "立即重启",
"update.restart_later": "稍后",
@@ -3134,6 +3175,9 @@
"update.up_to_date": "SolidCraft 已是最新版本。",
"update.update_now": "立即更新",
"update.you_have": "当前版本为 {version}。",
"web.open_full": "在 SolidCraft 中打开",
"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 关闭"
}
+13 -4
View File
@@ -296,9 +296,10 @@ fn opaque_depth(i: TOut, front: bool) -> AoDepth {
return AoDepth(depth, bitcast<u32>(depth));
}
// Deterministic cosine-weighted hemisphere samples. Projecting the sample and a point one
// radius toward the eye supplies a depth range in both orthographic and perspective views;
// distant foreground geometry cannot cast a dark halo onto a background surface.
// Cosine-weighted hemisphere samples, the pattern turned by a different angle at each pixel.
// Projecting the sample and a point one radius toward the eye supplies a depth range in both
// orthographic and perspective views; distant foreground geometry cannot cast a dark halo onto
// a background surface.
fn occlusion(i: TOut) -> f32 {
let radius = u.ao.x;
if (radius <= 0.0 || length(i.n) < 0.5) { return 1.0; }
@@ -311,6 +312,14 @@ fn occlusion(i: TOut) -> f32 {
// The map is a 1 × 1 stand-in until a frame draws into it (the product preview draws no
// prepass): sampling it tells nothing, so leave the surface unoccluded.
if (size.x < 2 || size.y < 2) { return 1.0; }
// The same offsets at every pixel made each sample darken its own shifted copy of a nearby
// part's outline: faint ghosts of the part where the radius is large on screen (#4167),
// stepped bands along contacts where it is small (#3407). Turned by interleaved gradient
// noise (Jimenez 2014), neighbouring pixels sample different points and the copies break up
// into an even grain.
let turn = fract(52.9829189 * fract(dot(i.pos.xy, vec2<f32>(0.06711056, 0.00583715))));
let t = tangent * cos(turn * 6.28318531) + bitangent * sin(turn * 6.28318531);
let b = cross(n, t);
// One comparison for both depth directions: depths read as 0 near / 1 far (mirrored when
// the buffer runs reversed). The map is cleared to its far plane either way, so a texel
// with nothing in it reads as 1 and the `d < 1.0` test below skips it.
@@ -319,7 +328,7 @@ fn occlusion(i: TOut) -> f32 {
for (var j = 0u; j < 32u; j = j + 1u) {
let f = (f32(j) + 0.5) / 32.0;
let angle = f32(j) * 2.39996323;
let direction = tangent * (cos(angle) * sqrt(f)) + bitangent * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
let direction = t * (cos(angle) * sqrt(f)) + b * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
let reach = radius * (0.2 + 0.8 * fract(f32(j) * 0.61803399 + 0.5));
let sample = i.wp + n * (radius * 0.025) + direction * reach;
let q = u.vp * vec4<f32>(sample, 1.0);
+9 -8
View File
@@ -155,9 +155,11 @@ impl Tokens {
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
origin_point: Color32::from_rgb(214, 215, 218),
axis_x: Color32::from_rgb(255, 0, 0),
axis_y: Color32::from_rgb(0, 255, 0),
axis_z: Color32::from_rgb(2, 2, 248),
// The one set of X, Y and Z colours (#976), equal to the engine's `view::colors::AXIS_*`
// so a headless picture draws its origin axes as the window does.
axis_x: Color32::from_rgb(220, 60, 60),
axis_y: Color32::from_rgb(60, 170, 60),
axis_z: Color32::from_rgb(60, 100, 230),
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
construction_geom: icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[0]),
body: Color32::from_rgb(176, 186, 198),
@@ -178,6 +180,7 @@ impl Tokens {
body_selected: Color32::from_rgb(110, 170, 235),
sketch: Color32::from_rgb(30, 90, 200),
sketch_fixed: Color32::from_rgb(20, 20, 20),
sketch_halo: Color32::from_rgb(240, 243, 247),
sketch_point: Color32::BLACK,
sketch_locked: Color32::from_rgb(0, 140, 60),
dimension: Color32::from_rgb(36, 40, 48),
@@ -219,8 +222,6 @@ impl Tokens {
sketch_lost: Color32::from_rgb(200, 60, 90),
sketch_construction: Color32::from_rgb(230, 130, 40),
sketch_centerline: Color32::from_rgb(200, 110, 60),
triad: [Color32::from_rgb(220, 60, 60), Color32::from_rgb(60, 170, 60), Color32::from_rgb(60, 100, 230)],
gizmo: [Color32::from_rgb(225, 70, 60), Color32::from_rgb(80, 190, 80), Color32::from_rgb(70, 130, 240)],
bend_up: Color32::from_rgb(30, 140, 50),
bend_down: Color32::from_rgb(200, 90, 20),
interference: Color32::from_rgb(235, 45, 40),
@@ -336,6 +337,7 @@ impl Tokens {
t.body_edge = Color32::from_rgb(16, 16, 18);
t.sketch = Color32::from_rgb(92, 150, 255);
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
t.sketch_halo = Color32::from_rgb(28, 32, 40);
t.sketch_point = Color32::from_rgb(232, 234, 238);
t.sketch_locked = Color32::from_rgb(96, 204, 116);
t.dimension = Color32::from_rgb(224, 228, 234);
@@ -375,9 +377,8 @@ impl Tokens {
let mut t = self;
let positive = [0.20, 0.45, 0.90];
let negative = [0.85, 0.40, 0.05];
t.triad[0] = Color32::from_rgb(200, 40, 40);
t.triad[1] = Color32::from_rgb(120, 210, 80);
t.gizmo[1] = Color32::from_rgb(140, 220, 90);
t.axis_x = Color32::from_rgb(200, 40, 40);
t.axis_y = Color32::from_rgb(120, 210, 80);
t.preview_sketch = Color32::from_rgb(80, 185, 145);
t.analysis.draft = [positive, [0.95, 0.90, 0.60], negative];
t.analysis.access = [positive, negative];
+245
View File
@@ -0,0 +1,245 @@
{
"base": "light",
"colors": {
"accent": "#c63a00",
"accent_soft": "#e8d0c3",
"agent_busy": "#015ba6",
"agent_ok": "#257231",
"app_bar": "#e1ddd8",
"app_bar_hover": "#13110e14",
"app_bar_hover_button": "#13110e18",
"app_bar_hover_strong": "#13110e22",
"app_bar_hover_subtle": "#13110e0c",
"app_bar_icon": [
"#403d38",
"#b5b0a9",
"#c63a00"
],
"app_bar_separator": "#b5b0a9",
"app_bar_tab_active": "#f9f6f2",
"app_bar_text": "#403d38",
"axis_x": "#c71529",
"axis_y": "#257231",
"axis_z": "#015ba6",
"bend_down": "#705e91",
"bend_up": "#257231",
"body": "#c2beb8",
"body_edge": "#403d38",
"body_selected": "#de8d6b",
"border": "#d9d5d0",
"box_crossing": "#257231",
"box_window": "#015ba6",
"chip": "#c63a00",
"construction_geom": "#7d6535",
"construction_plane": "#7d653540",
"continuity": [
"#257231",
"#015ba6",
"#c08a1a",
"#b14578"
],
"control_border": "#d4d0ca",
"control_fill": "#fffdfa",
"cube_border": "#b5b0a9",
"cube_face": "#fffdfa",
"cube_hover": "#d4d0ca",
"cube_text": "#13110e",
"dialog_bg": "#f9f6f2",
"dialog_body": "#f9f6f2",
"dialog_border": "#d4d0ca",
"dialog_footer": "#efece8",
"dialog_label": "#403d38",
"dialog_rule": "#d9d5d0",
"dimension": "#403d38",
"dimension_driven": "#6c6863",
"error": "#c71529",
"field": "#fffdfa",
"form_accent": "#5844bc",
"form_dim": "#6c6863",
"form_error": "#c71529",
"form_error_fill": "#f4e0de",
"form_fill": "#5844bc",
"form_ink": "#13110e",
"form_primary": "#c63a00",
"form_soft": "#e4e0e5",
"form_success": "#257231",
"form_success_fill": "#e4e9df",
"form_tab_ink": "#5844bc",
"gizmo": [
"#b00018",
"#096125",
"#004c9b"
],
"glyph_bg": "#fffdfa",
"glyph_edge": "#d4d0ca",
"glyph_text": "#604b83",
"grid": "#edeae5",
"grid_major": "#e1ddd8",
"heading_bg": "#ebe7e2",
"hover": "#e1ddd7",
"hover_edge": "#13110e",
"hover_edge_halo": "#fffdfa",
"hover_profile": "#13110e22",
"hover_profile_edge": "#403d38",
"icon": "#403d38",
"icon_fill": "#d4d0ca",
"icons.access": [
"#257231",
"#c71529"
],
"icons.agent": "#5844bc",
"icons.axes": [
"#c71529",
"#257231",
"#015ba6"
],
"icons.curvature": [
"#015ba6",
"#257231",
"#c71529"
],
"icons.draft": [
"#257231",
"#e6af2b"
],
"icons.floor": "#6c6863",
"icons.fold": "#8d8982",
"icons.folder": "#c9a96a",
"icons.glint": "#ffffffa0",
"icons.highlight": "#fffdfa",
"icons.interference": "#c71529",
"icons.marker_halo": "#13110e96",
"icons.paint": [
"#c71529",
"#015ba6",
"#e6af2b"
],
"icons.plane": "#e3cf9f",
"icons.sheet": "#b9cde2",
"icons.sheet_2": "#a5bfd9",
"icons.sheet_light": "#dbe6f0",
"icons.sheet_line": "#015ba6",
"icons.sky": "#b9cde2",
"icons.stripe": "#13110e",
"icons.warning": "#e6af2b",
"interference": "#c71529",
"logo": [
"#bb461e",
"#c63a00",
"#902e04"
],
"manipulator": "#004c9b",
"mesh_accent": "#7e6900",
"mesh_fill": "#7e6900",
"mesh_ink": "#6b5903",
"mesh_soft": "#e7e3d8",
"on_accent": "#fffbed",
"origin_plane": "#6c686338",
"origin_plane_edge": "#6c6863c8",
"origin_plane_hover": "#015ba670",
"origin_point": "#403d38",
"overlay": "#fffdfaeb",
"panel": "#efece8",
"panel_header": "#ebe7e2",
"preview_add": "#4f8fd6",
"preview_cut": "#c715296e",
"preview_new": "#e6af2b",
"preview_sketch": "#2d713b",
"profile_fill": "#015ba614",
"render_accent": "#ac1f6a",
"render_fill": "#ac1f6a",
"render_ink": "#ac1f6a",
"render_soft": "#eadedf",
"rubber_band": "#004c9b",
"section_cap": "#7d6535",
"sel_edge": "#c63a00",
"sel_edge_rim": "#e29c80",
"sel_face": "#d67147",
"sel_profile": "#c63a00a0",
"sel_profile_edge": "#c63a00",
"sel_vertex": "#c63a00",
"selected_bg": "#f1d8cb",
"selected_ink": "#13110e",
"sheet_fill": "#015ba6",
"sheet_ink": "#015ba6",
"sheet_soft": "#dee2e3",
"sketch": "#015ba6",
"sketch_accent": "#2d713b",
"sketch_centerline": "#403d38",
"sketch_construction": "#7d6535",
"sketch_fixed": "#13110e",
"sketch_ink": "#2d713b",
"sketch_locked": "#257231",
"sketch_lost": "#c71529",
"sketch_point": "#13110e",
"sketch_point_free": "#015ba6",
"sketch_projected": "#705e91",
"sketch_soft": "#e1e3dc",
"subheading_bg": "#efece8",
"surface_accent": "#7d6535",
"surface_fill": "#7d6535",
"surface_ink": "#6a562e",
"surface_soft": "#e7e3db",
"tab_on_fill_dark": "#13110e",
"text": "#13110e",
"text_dim": "#6c6863",
"timeline": "#efece8",
"timeline_item": "#fffdfa",
"timeline_item_error": "#f2c5c8",
"timeline_marker": "#c63a00",
"toolbar": "#efece8",
"triad": [
"#c71529",
"#257231",
"#015ba6"
],
"viewport_bottom": "#f1ede8",
"viewport_top": "#f9f6f2",
"warning": "#956000"
},
"format": "solidcraft-theme/1",
"name": "Akari",
"icon_palette": {
"ink": "#13110e",
"line": "#505054",
"passive-1": "#dee5ee",
"passive-2": "#aeb5bd",
"passive-3": "#81878f",
"guide": "#6e6e73",
"solid": [
"#dbe9fe",
"#81b3fa",
"#1875eb"
],
"surface": [
"#fde3c4",
"#daa662",
"#a06f28"
],
"mesh": [
"#fee7a2",
"#d1ad37",
"#937717"
],
"form": [
"#ede2ff",
"#c39afc",
"#8b62bf"
],
"sketch": [
"#b1fdc5",
"#72c78b",
"#388e56"
],
"constraint": [
"#fee0dd",
"#f98e87",
"#d73339"
],
"construction": [
"#fde3c4",
"#daa662",
"#a06f28"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#d4902f",
"form_success_fill": "#3b280d",
"form_tab_ink": "#cf8d31",
"gizmo": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"glyph_bg": "#201607",
"glyph_edge": "#e0d1ac",
"glyph_text": "#e0d1ac",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ba7e29",
"sketch_construction": "#ad7626",
"sketch_fixed": "#c68b2f",
"sketch_halo": "#170d03",
"sketch_ink": "#f7ce88",
"sketch_locked": "#d4902f",
"sketch_lost": "#fff0ca",
@@ -188,11 +184,6 @@
"timeline_item_error": "#46300f",
"timeline_marker": "#9b6922",
"toolbar": "#100b04",
"triad": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"viewport_bottom": "#090603",
"viewport_top": "#090603",
"warning": "#ffd274"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#6ee0bb",
"form_success_fill": "#153e47",
"form_tab_ink": "#b6a9e4",
"gizmo": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"glyph_bg": "#0a233e",
"glyph_edge": "#ff968d",
"glyph_text": "#ff968d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e7b374",
"sketch_construction": "#78b8dc",
"sketch_fixed": "#e3f0fa",
"sketch_halo": "#092c50",
"sketch_ink": "#9ddab7",
"sketch_locked": "#6ee0bb",
"sketch_lost": "#ff968d",
@@ -188,11 +184,6 @@
"timeline_item_error": "#422d40",
"timeline_marker": "#94b2cb",
"toolbar": "#0c223c",
"triad": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"viewport_bottom": "#143b67",
"viewport_top": "#143b67",
"warning": "#f0d18c"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a6e3a1",
"form_success_fill": "#353f42",
"form_tab_ink": "#b4befe",
"gizmo": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"glyph_bg": "#313244",
"glyph_edge": "#fc93b0",
"glyph_text": "#fc93b0",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f9e2af",
"sketch_construction": "#f9e2af",
"sketch_fixed": "#cdd6f4",
"sketch_halo": "#11111b",
"sketch_ink": "#a6e3a1",
"sketch_locked": "#a6e3a1",
"sketch_lost": "#f38ba8",
@@ -188,11 +184,6 @@
"timeline_item_error": "#51384b",
"timeline_marker": "#a6adc8",
"toolbar": "#1e1e2e",
"triad": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"viewport_bottom": "#11111b",
"viewport_top": "#11111b",
"warning": "#f9e2af"
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#b4b4b8",
"app_bar_tab_active": "#f4f4f4",
"app_bar_text": "#242426",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#e95049",
"axis_y": "#35a754",
"axis_z": "#297fdb",
"bend_down": "#bd843f",
"bend_up": "#36934d",
"body": "#b6bbc3",
@@ -65,11 +65,6 @@
"form_success": "#237b38",
"form_success_fill": "#e6f3e8",
"form_tab_ink": "#75559f",
"gizmo": [
"#ed514b",
"#36ad59",
"#2184e9"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#d73339",
"glyph_text": "#d73339",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b77c47",
"sketch_construction": "#cb8b37",
"sketch_fixed": "#262628",
"sketch_halo": "#ffffff",
"sketch_ink": "#236f40",
"sketch_locked": "#318348",
"sketch_lost": "#c94f68",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f8dedb",
"timeline_marker": "#63636b",
"toolbar": "#ececec",
"triad": [
"#e95049",
"#35a754",
"#297fdb"
],
"viewport_bottom": "#d9dce2",
"viewport_top": "#f5f5f7",
"warning": "#ad7400"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#83c69b",
"form_success_fill": "#35423e",
"form_tab_ink": "#a796cb",
"gizmo": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"glyph_bg": "#303338",
"glyph_edge": "#e57071",
"glyph_text": "#e57071",
@@ -168,6 +163,7 @@
"sketch_centerline": "#dbb879",
"sketch_construction": "#dbb879",
"sketch_fixed": "#e0e2e5",
"sketch_halo": "#17191d",
"sketch_ink": "#88b298",
"sketch_locked": "#83c69b",
"sketch_lost": "#ed7777",
@@ -188,11 +184,6 @@
"timeline_item_error": "#553a3d",
"timeline_marker": "#a0a6b0",
"toolbar": "#25272b",
"triad": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"viewport_bottom": "#17191d",
"viewport_top": "#17191d",
"warning": "#dbb879"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#39d45c",
"form_success_fill": "#133b20",
"form_tab_ink": "#36b95c",
"gizmo": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"glyph_bg": "#09200e",
"glyph_edge": "#afd5b6",
"glyph_text": "#afd5b6",
@@ -168,6 +163,7 @@
"sketch_centerline": "#32ba50",
"sketch_construction": "#2fad4a",
"sketch_fixed": "#8def9f",
"sketch_halo": "#021007",
"sketch_ink": "#bbffca",
"sketch_locked": "#39d45c",
"sketch_lost": "#d8ffdf",
@@ -188,11 +184,6 @@
"timeline_item_error": "#21462a",
"timeline_marker": "#5b9b6a",
"toolbar": "#061009",
"triad": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"viewport_bottom": "#010703",
"viewport_top": "#010703",
"warning": "#d1ffad"
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#141617",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +84,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#382c20",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#4e5954",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#4a3a2c",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#48544f",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#423326",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fbf8e3",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f3e6c6",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#f6edd0",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#edd7ad",
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fcf5d8",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f5e2b7",
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#40e080",
"form_success_fill": "#0b2616",
"form_tab_ink": "#df8fff",
"gizmo": [
"#ff6666",
"#40e080",
"#0070d0"
],
"glyph_bg": "#151515",
"glyph_edge": "#fa8a86",
"glyph_text": "#fa8a86",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ffcc00",
"sketch_construction": "#ffcc00",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#00ff80",
"sketch_locked": "#40e080",
"sketch_lost": "#ff6666",
@@ -188,11 +184,6 @@
"timeline_item_error": "#3d1818",
"timeline_marker": "#cccccc",
"toolbar": "#000000",
"triad": [
"#ff6666",
"#40e080",
"#0070d0"
],
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffcc00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a3be8c",
"form_success_fill": "#424b4d",
"form_tab_ink": "#c3a5bd",
"gizmo": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"glyph_bg": "#3b4252",
"glyph_edge": "#f9959c",
"glyph_text": "#f9959c",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ebcb8b",
"sketch_construction": "#ebcb8b",
"sketch_fixed": "#eceff4",
"sketch_halo": "#242f40",
"sketch_ink": "#a3be8c",
"sketch_locked": "#a3be8c",
"sketch_lost": "#bf616a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#513f4a",
"timeline_marker": "#a4b0c2",
"toolbar": "#2e3440",
"triad": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"viewport_bottom": "#242f40",
"viewport_top": "#242f40",
"warning": "#ebcb8b"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#50fa7b",
"form_success_fill": "#2f4d42",
"form_tab_ink": "#d6acff",
"gizmo": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"glyph_bg": "#343746",
"glyph_edge": "#fc827b",
"glyph_text": "#fc827b",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f1fa8c",
"sketch_construction": "#f1fa8c",
"sketch_fixed": "#f8f8f2",
"sketch_halo": "#1d1e29",
"sketch_ink": "#50fa7b",
"sketch_locked": "#50fa7b",
"sketch_lost": "#ff5555",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#a1a4c4",
"toolbar": "#282a36",
"triad": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"viewport_bottom": "#1d1e29",
"viewport_top": "#1d1e29",
"warning": "#f1fa8c"
+245
View File
@@ -0,0 +1,245 @@
{
"base": "dark",
"colors": {
"accent": "#ff7c50",
"accent_soft": "#3a2723",
"agent_busy": "#6daff7",
"agent_ok": "#89cf8e",
"app_bar": "#0f1113",
"app_bar_hover": "#e1e3e514",
"app_bar_hover_button": "#e1e3e518",
"app_bar_hover_strong": "#e1e3e522",
"app_bar_hover_subtle": "#e1e3e50c",
"app_bar_icon": [
"#bbbec1",
"#585b5f",
"#ff7c50"
],
"app_bar_separator": "#474b4f",
"app_bar_tab_active": "#1b1e20",
"app_bar_text": "#bbbec1",
"axis_x": "#f85052",
"axis_y": "#89cf8e",
"axis_z": "#6daff7",
"bend_down": "#ab9acd",
"bend_up": "#89cf8e",
"body": "#74777b",
"body_edge": "#0f1113",
"body_selected": "#c56241",
"border": "#2f3235",
"box_crossing": "#89cf8e",
"box_window": "#6daff7",
"chip": "#ff7c50",
"construction_geom": "#ceb687",
"construction_plane": "#ceb68740",
"continuity": [
"#89cf8e",
"#6daff7",
"#e6af2b",
"#f38bb7"
],
"control_border": "#474b4f",
"control_fill": "#2c2e31",
"cube_border": "#585b5f",
"cube_face": "#2c2e31",
"cube_hover": "#474b4f",
"cube_text": "#e1e3e5",
"dialog_bg": "#222527",
"dialog_body": "#222527",
"dialog_border": "#383b3e",
"dialog_footer": "#1b1e20",
"dialog_label": "#bbbec1",
"dialog_rule": "#2f3235",
"dimension": "#bbbec1",
"dimension_driven": "#8f9296",
"error": "#f85052",
"field": "#2c2e31",
"form_accent": "#a6a2fc",
"form_dim": "#8f9296",
"form_error": "#ff6060",
"form_error_fill": "#37292b",
"form_fill": "#a6a2fc",
"form_ink": "#e1e3e5",
"form_primary": "#ff7c50",
"form_soft": "#2f3043",
"form_success": "#89cf8e",
"form_success_fill": "#2c3631",
"form_tab_ink": "#a6a2fc",
"gizmo": [
"#ff6060",
"#98dfa2",
"#7abfff"
],
"glyph_bg": "#222527",
"glyph_edge": "#474b4f",
"glyph_text": "#ab9acd",
"grid": "#27292c",
"grid_major": "#2c2e31",
"heading_bg": "#15171a",
"hover": "#383b3e",
"hover_edge": "#0f1113",
"hover_edge_halo": "#e1e3e5",
"hover_profile": "#e1e3e522",
"hover_profile_edge": "#bbbec1",
"icon": "#bbbec1",
"icon_fill": "#474b4f",
"icons.access": [
"#89cf8e",
"#f85052"
],
"icons.agent": "#aba3ff",
"icons.axes": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"icons.curvature": [
"#6daff7",
"#89cf8e",
"#f85052"
],
"icons.draft": [
"#89cf8e",
"#e6af2b"
],
"icons.floor": "#383b3e",
"icons.fold": "#707376",
"icons.folder": "#ceb687",
"icons.glint": "#ffffffa0",
"icons.highlight": "#e1e3e5",
"icons.interference": "#f85052",
"icons.marker_halo": "#0f111396",
"icons.paint": [
"#f85052",
"#6daff7",
"#e6af2b"
],
"icons.plane": "#dcc9a0",
"icons.sheet": "#a2bedb",
"icons.sheet_2": "#8fb0d2",
"icons.sheet_light": "#c9d9ea",
"icons.sheet_line": "#6daff7",
"icons.sky": "#a2bedb",
"icons.stripe": "#0f1113",
"icons.warning": "#e6af2b",
"interference": "#f85052",
"logo": [
"#ff9c7c",
"#ff7c50",
"#b75c3e"
],
"manipulator": "#7abfff",
"mesh_accent": "#cfc152",
"mesh_fill": "#cfc152",
"mesh_ink": "#cfc152",
"mesh_soft": "#363624",
"on_accent": "#0f1113",
"origin_plane": "#8f929638",
"origin_plane_edge": "#8f9296c8",
"origin_plane_hover": "#6daff770",
"origin_point": "#bbbec1",
"overlay": "#15171af0",
"panel": "#15171a",
"panel_header": "#222527",
"preview_add": "#4c78a7",
"preview_cut": "#f850526e",
"preview_new": "#e6af2b",
"preview_sketch": "#8eca96",
"profile_fill": "#6daff71c",
"render_accent": "#f38bb7",
"render_fill": "#f38bb7",
"render_ink": "#f38bb7",
"render_soft": "#3d2c36",
"rubber_band": "#7abfff",
"section_cap": "#ceb687",
"sel_edge": "#ff7c50",
"sel_edge_rim": "#ffbea8",
"sel_face": "#dd6d47",
"sel_profile": "#ff7c50b0",
"sel_profile_edge": "#ff7c50",
"sel_vertex": "#ff7c50",
"selected_bg": "#45332e",
"selected_ink": "#e1e3e5",
"sheet_fill": "#6daff7",
"sheet_ink": "#6daff7",
"sheet_soft": "#253242",
"sketch": "#6daff7",
"sketch_accent": "#8eca96",
"sketch_centerline": "#bbbec1",
"sketch_construction": "#ceb687",
"sketch_fixed": "#e1e3e5",
"sketch_ink": "#8eca96",
"sketch_locked": "#89cf8e",
"sketch_lost": "#f85052",
"sketch_point": "#e1e3e5",
"sketch_point_free": "#6daff7",
"sketch_projected": "#998cb3",
"sketch_soft": "#2b3730",
"subheading_bg": "#1b1e20",
"surface_accent": "#ceb687",
"surface_fill": "#ceb687",
"surface_ink": "#ceb687",
"surface_soft": "#36342e",
"tab_on_fill_dark": "#0f1113",
"text": "#e1e3e5",
"text_dim": "#8f9296",
"timeline": "#15171a",
"timeline_item": "#222527",
"timeline_item_error": "#552f31",
"timeline_marker": "#ff7c50",
"toolbar": "#15171a",
"triad": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"viewport_bottom": "#1b1e20",
"viewport_top": "#222527",
"warning": "#e6af2b"
},
"format": "solidcraft-theme/1",
"name": "Okibi",
"icon_palette": {
"ink": "#15171a",
"line": "#e1e3e5",
"passive-1": "#bfc1c5",
"passive-2": "#939599",
"passive-3": "#6a6c6f",
"guide": "#a0a6b0",
"solid": [
"#a0d7fb",
"#65bdf3",
"#4aa3d8"
],
"surface": [
"#fcc28e",
"#dea572",
"#c38c59"
],
"mesh": [
"#e8cd7d",
"#cab060",
"#b19747"
],
"form": [
"#d5c4fd",
"#b9a7df",
"#a08fc4"
],
"sketch": [
"#b0dbc0",
"#94bea4",
"#7ba58b"
],
"constraint": [
"#febbb9",
"#fb8e8d",
"#e57071"
],
"construction": [
"#fcc28e",
"#dea572",
"#c38c59"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#8fdb9a",
"form_success_fill": "#2c4a33",
"form_tab_ink": "#c97fdf",
"gizmo": [
"#e06c75",
"#98c379",
"#61afef"
],
"glyph_bg": "#282c34",
"glyph_edge": "#f57f87",
"glyph_text": "#f57f87",
@@ -168,6 +163,7 @@
"sketch_centerline": "#d19a66",
"sketch_construction": "#d19a66",
"sketch_fixed": "#abb2bf",
"sketch_halo": "#181a1f",
"sketch_ink": "#98c379",
"sketch_locked": "#98c379",
"sketch_lost": "#e06c75",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#abb2bf",
"toolbar": "#21252b",
"triad": [
"#e06c75",
"#98c379",
"#61afef"
],
"viewport_bottom": "#282c34",
"viewport_top": "#282c34",
"warning": "#e5c07b"
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#aaaaaa",
"app_bar_tab_active": "#e6e6e6",
"app_bar_text": "#222222",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#dc3c3c",
"axis_y": "#3caa3c",
"axis_z": "#3c64e6",
"bend_down": "#c85a14",
"bend_up": "#1e8c32",
"body": "#adadb6",
@@ -65,11 +65,6 @@
"form_success": "#336633",
"form_success_fill": "#e2ecdf",
"form_tab_ink": "#6d4e7d",
"gizmo": [
"#e1463c",
"#50be50",
"#4682f0"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#ab2125",
"glyph_text": "#ab2125",
@@ -168,6 +163,7 @@
"sketch_centerline": "#c86e3c",
"sketch_construction": "#e68228",
"sketch_fixed": "#222222",
"sketch_halo": "#ffffff",
"sketch_ink": "#456045",
"sketch_locked": "#447744",
"sketch_lost": "#c83c5a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f2dddd",
"timeline_marker": "#666666",
"toolbar": "#dddddd",
"triad": [
"#dc3c3c",
"#3caa3c",
"#3c64e6"
],
"viewport_bottom": "#ffffff",
"viewport_top": "#ffffff",
"warning": "#997722"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#859900",
"form_success_fill": "#dcdbb1",
"form_tab_ink": "#565a9d",
"gizmo": [
"#dc322f",
"#859900",
"#346e88"
],
"glyph_bg": "#e3ddc9",
"glyph_edge": "#da2f2d",
"glyph_text": "#da2f2d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b58900",
"sketch_construction": "#b58900",
"sketch_fixed": "#586e75",
"sketch_halo": "#fdf6e3",
"sketch_ink": "#5a6800",
"sketch_locked": "#859900",
"sketch_lost": "#dc322f",
@@ -188,11 +184,6 @@
"timeline_item_error": "#eabcad",
"timeline_marker": "#657b83",
"toolbar": "#eee8d5",
"triad": [
"#dc322f",
"#859900",
"#346e88"
],
"viewport_bottom": "#fdf6e3",
"viewport_top": "#fdf6e3",
"warning": "#b58900"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#008000",
"form_success_fill": "#c0dcc0",
"form_tab_ink": "#7d007d",
"gizmo": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#800000",
"glyph_text": "#800000",
@@ -168,6 +163,7 @@
"sketch_centerline": "#808000",
"sketch_construction": "#808000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
"sketch_ink": "#005400",
"sketch_locked": "#008000",
"sketch_lost": "#800080",
@@ -188,11 +184,6 @@
"timeline_item_error": "#ffffc0",
"timeline_marker": "#000000",
"toolbar": "#c0c0c0",
"triad": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"viewport_bottom": "#008080",
"viewport_top": "#008080",
"warning": "#ffff00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#9ece6a",
"form_success_fill": "#303932",
"form_tab_ink": "#bb9af7",
"gizmo": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"glyph_bg": "#24283b",
"glyph_edge": "#f6758d",
"glyph_text": "#f6758d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e0af68",
"sketch_construction": "#e0af68",
"sketch_fixed": "#c0caf5",
"sketch_halo": "#151722",
"sketch_ink": "#9ece6a",
"sketch_locked": "#9ece6a",
"sketch_lost": "#f7768e",
@@ -188,11 +184,6 @@
"timeline_item_error": "#4f313f",
"timeline_marker": "#9aa5ce",
"toolbar": "#1a1b26",
"triad": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"viewport_bottom": "#151722",
"viewport_top": "#151722",
"warning": "#e0af68"
+17 -11
View File
@@ -227,6 +227,22 @@ impl FeatureKind {
}
}
/// [`Document::component_transform`] with each component's first occurrence found by `first`
/// (an index, where many transforms are wanted at once).
pub(crate) fn transform_through<'a>(component: u64, first: impl Fn(u64) -> Option<&'a Occurrence>) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = first(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
}
impl Document {
/// The first occurrence of a component (its placement for authoring).
pub fn occurrence_of(&self, component: u64) -> Option<&crate::Occurrence> {
@@ -235,17 +251,7 @@ impl Document {
/// World transform of a component's frame (through its first occurrence and its parents').
pub fn component_transform(&self, component: u64) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = self.occurrence_of(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
transform_through(component, |c| self.occurrence_of(c))
}
/// One placement: the component the occurrence `path` from the root leads to (the root for
+2
View File
@@ -130,6 +130,7 @@ impl Document {
/// A column's current value in the design.
pub fn config_current(&self, c: &Column) -> Option<String> {
match c {
Column::Param(n) if self.text_param(n).is_some() => self.text_param(n).map(|p| p.text.clone()),
Column::Param(n) => self.all_param_exprs().into_iter().find(|(m, _, _)| m == n).map(|(_, e, kind)| {
// Feature literals are stored in base units, unlike newly typed bare
// row cells. Capture their unit explicitly before editing in an inch design.
@@ -262,6 +263,7 @@ impl Document {
}
self.configs.columns.iter().zip(&r.cells).all(|(c, v)| match c {
Column::Suppress(id) => suppression_cell(v).is_ok_and(|on| self.feature(*id).is_some_and(|f| f.suppressed == on)),
Column::Param(name) if self.text_param(name).is_some() => self.text_param(name).is_some_and(|p| p.text == crate::text_params::unquote(v)),
Column::Param(name) => {
let kind = self
.features
+28 -6
View File
@@ -1086,7 +1086,7 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Bend radius")]
radius: Option<Expr>,
/// inside | outside | middle.
/// inside | outside | middle | material_outside | tangent (`sheet::SheetBody::add_flanges_each`).
#[serde(default = "inside_str")]
position: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
@@ -1102,6 +1102,11 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Corner relief")]
corner_relief: Option<Expr>,
/// Where the heights are measured from: `inner` (the inner virtual sharp) or `tangent`
/// (past 90° the line square to the flange touching the bend's outside); none is the outer
/// virtual sharp (`sheet::HeightFrom`).
#[serde(default, skip_serializing_if = "Option::is_none")]
height_from: Option<String>,
},
/// Flat hems on sheet edges.
SheetHem {
@@ -2513,6 +2518,9 @@ pub struct Document {
#[serde(default, skip_serializing_if = "crate::Metadata::is_empty")]
pub metadata: crate::Metadata,
pub params: Vec<Param>,
/// Named texts the words of sketch texts can be built from ([`crate::text_params`]).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub text_params: Vec<crate::text_params::TextParam>,
pub features: Vec<Feature>,
/// Timeline marker: features at or after this index are rolled back (not evaluated).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2786,6 +2794,7 @@ impl Document {
units: default_units(),
metadata: Default::default(),
params: Vec::new(),
text_params: Vec::new(),
features: Vec::new(),
marker: None,
next_id: 1,
@@ -3469,6 +3478,12 @@ impl Document {
/// Add or change a parameter. Fails if the result would not evaluate. A new parameter
/// without a unit takes the unit of its value (mm, deg or none).
pub fn set_param(&mut self, name: &str, expr_s: &str, unit: Option<&str>, comment: Option<&str>) -> Result<()> {
// A text parameter: one with the text unit, or an existing one when no unit is given.
match (unit.map(crate::text_params::is_text_unit), self.text_param(name).is_some()) {
(Some(true), _) | (None, true) => return self.set_text_param(name, expr_s, comment),
(Some(false), true) => return Err(DocError::Invalid(format!("`{name}` is a text parameter"))),
_ => {}
}
if let Some(p) = Self::param_name_problem(name) {
return Err(DocError::Invalid(p));
}
@@ -3689,14 +3704,14 @@ impl Document {
Ok(())
}
/// Give the texts of a sketch the numbers their expressions come to (in place): a text's
/// height, place and angle that were typed as expressions. A text's outlines are rebuilt from
/// its numbers when the sketch's links are refreshed, so this comes before that. A number
/// typed as a number is left as it is.
/// Give the texts of a sketch the numbers and words their expressions come to (in place): a
/// text's height, place and angle that were typed as expressions, and its words built from
/// text parameters. A text's outlines are rebuilt from them when the sketch's links are
/// refreshed, so this comes before that. What was typed as such is left as it is.
pub fn apply_text_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
use crate::eval::val;
for link in &mut sk.links {
let solidcraft_sketch::LinkSource::Text { at, height, angle, exprs, .. } = &mut link.source else { continue };
let solidcraft_sketch::LinkSource::Text { text, at, height, angle, exprs, .. } = &mut link.source else { continue };
let number = |e: &Option<String>, kind: Kind, what: &str| -> Result<Option<f64>> {
match e {
None => Ok(None),
@@ -3718,6 +3733,13 @@ impl Document {
if exprs.height.is_some() && !(*height > 1e-6 && *height < 1e5) {
return Err(DocError::Invalid("a text's height must be positive".into()));
}
if let Some(e) = &exprs.text {
let words = self.eval_words(e).map_err(|error| DocError::Expr(format!("text: {error}")))?;
if words.trim().is_empty() || words.chars().count() > solidcraft_sketch::MAX_TEXT_CHARS {
return Err(DocError::Invalid(format!("text: `{e}` gives no words, or too many")));
}
*text = words;
}
}
Ok(())
}
+108 -40
View File
@@ -331,6 +331,8 @@ pub struct ThreadInfo {
pub body: String,
pub designation: String,
pub major_diameter: f64,
/// Radius of the actual cylindrical face when the cosmetic thread is evaluated.
pub radius: f64,
pub pitch: f64,
pub internal: bool,
pub axis_point: Vec3,
@@ -483,6 +485,7 @@ fn thread_on_impl(
body,
designation: name,
major_diameter: major,
radius: cyl.radius,
pitch,
internal: cyl.internal,
axis_point: cyl.axis_point,
@@ -1175,19 +1178,20 @@ pub struct Model {
}
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
/// sketch's texts), sorted.
/// sketch's texts, the text parameters of their words included), sorted.
fn param_names(f: &Feature) -> Vec<String> {
let mut names: Vec<String> = f.kind.expressions().iter().flat_map(|e| expr::references(e)).collect();
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
names.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
names.extend(sketch.text_exprs().into_iter().flat_map(|(_, e)| expr::references(e)));
names.extend(sketch.text_word_exprs().into_iter().flat_map(|e| crate::text_params::references(e)));
}
names.sort();
names.dedup();
names
}
fn fingerprint(prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_back: bool, steps: &StepDigests) -> u64 {
fn fingerprint(doc: &Document, prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_back: bool, steps: &StepDigests) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
prev.hash(&mut h);
rolled_back.hash(&mut h);
@@ -1195,13 +1199,14 @@ fn fingerprint(prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_ba
hash_feature_json(f, steps, &mut h);
for n in param_names(f) {
n.hash(&mut h);
match vals.get(&n) {
Some(v) => {
match (vals.get(&n), doc.text_param(&n)) {
(Some(v), _) => {
v.v.to_bits().hash(&mut h);
v.len.hash(&mut h);
v.ang.hash(&mut h);
}
None => 0u8.hash(&mut h),
(None, Some(p)) => p.text.hash(&mut h),
(None, None) => 0u8.hash(&mut h),
}
}
h.finish()
@@ -1378,7 +1383,7 @@ fn chain_fingerprint(
steps: &StepDigests,
st: Option<&ModelState>,
) -> u64 {
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f, st);
let mut fp = fingerprint(doc, prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f, st);
// Sheet metal features also depend on the rules (and the parameters they use).
if f.kind.is_sheet() {
let mut h = std::collections::hash_map::DefaultHasher::new();
@@ -1417,7 +1422,7 @@ fn context_digest(doc: &Document, vals: &BTreeMap<String, Value>) -> u64 {
for f in doc.features.iter().filter(|f| {
matches!(f.kind, FeatureKind::ConstructionPlane { .. } | FeatureKind::ConstructionAxis { .. } | FeatureKind::ConstructionPoint { .. })
}) {
fingerprint(0, f, vals, false, &StepDigests::default()).hash(&mut h);
fingerprint(doc, 0, f, vals, false, &StepDigests::default()).hash(&mut h);
}
doc.units.hash(&mut h);
serde_json::to_string(&doc.sheet).unwrap_or_default().hash(&mut h);
@@ -1986,7 +1991,8 @@ impl Model {
Arc::make_mut(&mut state).reserved_body_names.clone_from(&reserved);
}
// The same feature on the same inputs: its earlier result.
let key = cache_key(f, fp, fingerprint(0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
let key =
cache_key(f, fp, fingerprint(doc, 0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
let kept = self.kept.0.as_ref().filter(|_| !self.no_cache);
if !self.no_cache
&& let Some(c) = self.cache.lock().ok().and_then(|mut m| m.get(key)).or_else(|| kept.and_then(|k| k.get(key)))
@@ -2598,7 +2604,15 @@ fn intersection_tool_union(tools: Vec<Traced>) -> Result<Vec<Traced>> {
}
/// After a cut handled some sheet bodies as flat machining, apply it to the other targets.
fn apply_beside_sheet_cut(st: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Operation, targets: &[String], handled: &[String]) -> Result<()> {
fn apply_beside_sheet_cut(
doc: &Document,
st: &mut ModelState,
f: &Feature,
tools: Vec<Traced>,
op: Operation,
targets: &[String],
handled: &[String],
) -> Result<()> {
let effective = if targets.is_empty() { &f.participants } else { targets };
let all = effective.len() == 1 && effective.first().is_some_and(|t| t == "*");
let rest: Vec<String> = st
@@ -2613,7 +2627,7 @@ fn apply_beside_sheet_cut(st: &mut ModelState, f: &Feature, tools: Vec<Traced>,
if rest.is_empty() {
return Ok(());
}
apply_traced(st, f, tools, op, &rest)
apply_traced(doc, st, f, tools, op, &rest)
}
/// Only one target meets every separated tool. A tool with no contact must retain
@@ -2639,8 +2653,8 @@ fn common_join_target(state: &ModelState, indices: &[usize], tools: &[Traced]) -
Ok(common.filter(|index| contacts.iter().all(|contact| *contact == Some(*index))))
}
fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
apply_traced(state, f, tools.into_iter().map(Traced::unnamed).collect(), op, targets)
fn apply_op(doc: &Document, state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
apply_traced(doc, state, f, tools.into_iter().map(Traced::unnamed).collect(), op, targets)
}
/// Many cut tools clear of each other (a pattern's copies, the circles of a vent field in one
@@ -2685,8 +2699,60 @@ pub(crate) fn boolean_scope(state: &ModelState, f: &Feature, targets: &[String])
(!all && !effective.is_empty(), names)
}
/// Suggest one nearby body without changing the default component scope or running a boolean.
fn empty_boolean_targets(doc: &Document, state: &ModelState, f: &Feature, tools: &[Traced], default_targets: bool) -> DocError {
let plain = "there is no body to cut or intersect";
if !default_targets {
return DocError::Invalid(plain.into());
}
let component_name = |id| {
if id == 0 {
doc.name.clone()
} else {
doc.components.iter().take(10_000).find(|c| c.id == id).map(|c| c.name.clone()).unwrap_or_else(|| format!("Component{id}"))
}
};
let tool_bounds: Vec<_> = tools.iter().take(64).map(|t| t.body.outline_bounds()).collect();
let frame = doc.component_transform(f.component);
let Some(inverse) = crate::mat_inverse(&frame) else { return DocError::Invalid(plain.into()) };
// Named by the component the Browser shows the body in; one shown in the feature's own
// component has no other component to point at.
let others = state
.bodies
.iter()
.take(256)
.map(|b| (b, doc.model_body_component(b)))
.filter(|(b, owner)| b.component != f.component && *owner != f.component);
for (body, owner) in others {
// Compare in the feature's frame, including component placements and body offsets.
let world = body_world(doc, body);
let relative = crate::mat_mul(&inverse, &world);
let bounds = body.outline_bounds();
if bounds.is_empty() || !bounds.min.is_finite() || !bounds.max.is_finite() {
continue;
}
let mut placed = solidcraft_geom::Aabb3::EMPTY;
for x in [bounds.min.x, bounds.max.x] {
for y in [bounds.min.y, bounds.max.y] {
for z in [bounds.min.z, bounds.max.z] {
placed.add(crate::apply_point(&relative, Vec3::new(x, y, z)));
}
}
}
if tool_bounds.iter().any(|b| !b.is_empty() && b.min.is_finite() && b.max.is_finite() && b.meets(&placed)) {
let own = component_name(f.component);
let other = component_name(owner);
let name = &body.name;
return DocError::Invalid(format!(
"{plain} in component '{own}'; the tool may overlap '{name}' in component '{other}' — activate that component or pick the body as a target"
));
}
}
DocError::Invalid(plain.into())
}
/// [`apply_op`] with tools whose lineage is known: the bodies it makes or changes keep theirs.
fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Operation, targets: &[String]) -> Result<()> {
fn apply_traced(doc: &Document, state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Operation, targets: &[String]) -> Result<()> {
let tools = if op == Operation::Intersect { intersection_tool_union(tools)? } else { tools };
// Participants: the feature's own targets, else its explicit participants, else the bodies
// of its component (and what it made itself): other components are left alone.
@@ -2706,7 +2772,7 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
};
if op == Operation::NewBody || target_idx.is_empty() {
if op == Operation::Cut || op == Operation::Intersect {
return Err(DocError::Invalid("there is no body to cut or intersect".into()));
return Err(empty_boolean_targets(doc, state, f, &tools, targets.is_empty()));
}
for (k, t) in tools.into_iter().enumerate() {
let name = new_name(state, f, k);
@@ -2752,7 +2818,7 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
let mut batched = state.clone();
// Preserve the original participants. For Join, the compound's larger
// size must not expand the contact tolerance and absorb a nearby body.
if apply_traced(&mut batched, f, vec![Traced::step(compound, &tools.iter().collect::<Vec<_>>())], op, &names).is_ok() {
if apply_traced(doc, &mut batched, f, vec![Traced::step(compound, &tools.iter().collect::<Vec<_>>())], op, &names).is_ok() {
*state = batched;
return Ok(());
}
@@ -6298,12 +6364,12 @@ fn replay(
}
// New bodies from a pattern join their source when they touch it (as one feature would).
if f.body_names.is_empty() {
apply_traced(st, f, copies, op, &targets)?;
apply_traced(doc, st, f, copies, op, &targets)?;
} else {
let start = st.bodies.iter().filter(|b| b.feature == f.id).count();
let mut named = f.clone();
named.body_names = f.body_names.iter().skip(start).cloned().collect();
apply_traced(st, &named, copies, op, &targets)?;
apply_traced(doc, st, &named, copies, op, &targets)?;
}
}
if missed > 0 {
@@ -6784,10 +6850,10 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
let handled = more::sheet_cut(f, st, &bodies, &crate::plain(targets), constructor.as_ref(), warning)?;
if !handled.is_empty() {
return apply_beside_sheet_cut(st, f, tools, *operation, &crate::plain(targets), &handled);
return apply_beside_sheet_cut(doc, st, f, tools, *operation, &crate::plain(targets), &handled);
}
}
apply_traced(st, f, tools, *operation, &crate::plain(targets))
apply_traced(doc, st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::Hole { thread, thread_depth, diameter, .. } => {
let thread_depth = thread_depth.as_ref().map(|d| val(vals, d, Kind::Length)).transpose()?;
@@ -6802,9 +6868,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let handled = sheet_hole_cut(st, f, &tools, warning)?;
let tools: Vec<Traced> = tools.into_iter().map(|(tool, _)| tool).collect();
if handled.is_empty() {
apply_traced(st, f, tools, Operation::Cut, &[])?;
apply_traced(doc, st, f, tools, Operation::Cut, &[])?;
} else {
apply_beside_sheet_cut(st, f, tools, Operation::Cut, &[], &handled)?;
apply_beside_sheet_cut(doc, st, f, tools, Operation::Cut, &[], &handled)?;
}
if let Some(d) = thread {
// Find the bore beyond any entry recess, including shallow and through holes.
@@ -7011,7 +7077,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
if *remove_tools {
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
apply_traced(st, f, made, *operation, &[])
apply_traced(doc, st, f, made, *operation, &[])
}
FeatureKind::SplitFace { faces, body, plane, tool, sketch } => {
let i = body_at(st, body, &crate::plain(faces))?;
@@ -7244,9 +7310,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation, false, warning)?;
let tools = vec![crate::lineage::box_tool(b)];
if !handled.is_empty() {
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
return apply_beside_sheet_cut(doc, st, f, tools, *operation, &[], &handled);
}
apply_traced(st, f, tools, *operation, &[])
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)?);
@@ -7256,17 +7322,17 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false, warning)?;
let tools = vec![crate::lineage::cylinder_tool(b)];
if !handled.is_empty() {
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
return apply_beside_sheet_cut(doc, st, f, tools, *operation, &[], &handled);
}
apply_traced(st, f, tools, *operation, &[])
apply_traced(doc, st, f, tools, *operation, &[])
}
FeatureKind::Sphere { center, radius, operation } => {
let b = kernel::sphere(point(vals, center)?, val(vals, radius, Kind::Length)?)?;
apply_op(st, f, vec![b], *operation, &[])
apply_op(doc, st, f, vec![b], *operation, &[])
}
FeatureKind::Torus { center, major, minor, operation } => {
let b = kernel::torus(point(vals, center)?, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?;
apply_op(st, f, vec![b], *operation, &[])
apply_op(doc, st, f, vec![b], *operation, &[])
}
FeatureKind::Combine { target, tools, operation, keep_tools } => {
if tools.iter().any(|t| t == target) {
@@ -7314,7 +7380,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
return Ok(()); // the pieces were all joined back into the body they are part of
}
let op = if *operation == Operation::NewBody { Operation::Join } else { *operation };
apply_traced(st, f, tb, op, std::slice::from_ref(&target.0))?;
apply_traced(doc, st, f, tb, op, std::slice::from_ref(&target.0))?;
if !keep_tools {
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
@@ -8033,17 +8099,19 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
let geom = if ident { c.geom.clone() } else { c.geom.placed(&m) };
out.construct.push(crate::construct::Construct { geom, ..c.clone() });
}
for t in st
.threads
.iter()
.filter(|t| st.body(&t.body).map(|body| doc.model_body_component(body)).unwrap_or_else(|| comp_of_feature(t.feature)) == comp)
{
let mut t2 = t.clone();
let frame = st.body(&t.body).and_then(|body| doc.model_body_offset(body)).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m);
t2.axis_point = crate::apply_point(&frame, t.axis_point);
t2.axis = crate::apply_vector(&frame, *t.axis).unit().unwrap_or(t.axis);
out.threads.push(t2);
}
}
// Unlike editable sketch guides, cosmetic surfaces belong to every occurrence.
for t in st
.threads
.iter()
.filter(|t| st.body(&t.body).map(|body| doc.model_body_component(body)).unwrap_or_else(|| comp_of_feature(t.feature)) == comp)
{
let mut t2 = t.clone();
t2.body = doc.world_body_name(st, &t.body, comp, &path);
let frame = st.body(&t.body).and_then(|body| doc.model_body_offset(body)).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m);
t2.axis_point = crate::apply_point(&frame, t.axis_point);
t2.axis = crate::apply_vector(&frame, *t.axis).unit().unwrap_or(t.axis);
out.threads.push(t2);
}
}
*placed_before = placed_now;
+15 -10
View File
@@ -67,11 +67,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if st.bodies.is_empty() {
return Err(DocError::Invalid("there is no body to emboss".into()));
}
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
apply_op(doc, st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
}
FeatureKind::Rib { .. } => {
let tools = rib_tools(vals, st, f, None)?;
apply_op(st, f, tools, Operation::Join, &[])
apply_op(doc, st, f, tools, Operation::Join, &[])
}
FeatureKind::ReplaceFace { faces, target, body } => {
let tp = doc.resolve_plane_in(vals, target, 0, Some(st))?;
@@ -561,7 +561,7 @@ pub(super) fn sheet_eval(
st.sheets.push(sh);
Ok(())
}
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief, height_from } => {
let i = sheet_for(st, body, edges.first().map(|e| e.point.0))?;
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
// Each edge's own height and angle, else the feature's.
@@ -598,6 +598,11 @@ pub(super) fn sheet_eval(
None => None,
},
};
let from = match height_from {
Some(name) => sheet::HeightFrom::parse(name)
.ok_or_else(|| DocError::Invalid(format!("unknown flange height datum `{name}` (outer, inner or tangent)")))?,
None => sheet::HeightFrom::Outer,
};
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
if sh.flat {
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
@@ -606,7 +611,7 @@ pub(super) fn sheet_eval(
let each: Vec<sheet::EdgeFlange> = sheet_picks(sh, f, &points, warning)
.into_iter()
.zip(own)
.map(|(pick, (height, angle))| sheet::EdgeFlange { pick, height, angle })
.map(|(pick, (height, angle))| sheet::EdgeFlange { pick, height, angle, from })
.collect();
let n0 = sh.flanges.len();
let (_, notes) = sh.add_flanges_each(&each, rr, position, *flip, relief)?;
@@ -1884,7 +1889,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?.into_iter().map(|b| crate::lineage::part_tool(b, &format!("rib{k}"))));
}
}
apply_traced(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
apply_traced(doc, st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
// The root fillet: rounds the circle where the post meets the face.
if rf > 1e-9 {
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
@@ -1908,7 +1913,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let hp = Plane::new(position.0, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
apply_traced(doc, st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
}
Ok(())
}
@@ -1994,10 +1999,10 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let delta = (h * 0.05).clamp(1e-3, 0.5);
if *groove {
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?.into_iter().map(|b| crate::lineage::part_tool(b, "groove")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Cut, std::slice::from_ref(&target))
} else {
let tool = kernel::extrude(&plane, &[band], -delta, h)?.into_iter().map(|b| crate::lineage::part_tool(b, "lip")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
@@ -2023,7 +2028,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let (lo, hi) = if sgn > 0.0 { (0.0, w) } else { (-w, 0.0) };
let tool = kernel::extrude(&pl, &[Region2 { outer: lp, holes: Vec::new() }], lo, hi)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "arm")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, position.0)?;
@@ -2074,7 +2079,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
kernel::extrude(&base, &[region], 0.0, h + delta)?
};
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "rest")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
_ => Err(DocError::Invalid(format!("{} is not a plastic feature", f.name))),
}
+1 -1
View File
@@ -65,7 +65,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if tools.is_empty() {
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
}
apply_op(st, f, tools, *operation, &crate::plain(targets))
apply_op(doc, st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::SurfaceTrim { body, plane, tool, keep, keep_more, curves, with, keep_with, stitch, extend } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
+1
View File
@@ -1419,6 +1419,7 @@ mod tests {
body: None,
corner_relief_shape: shape.map(Into::into),
corner_relief: size.map(Into::into),
height_from: None,
};
let saved = |document: &Document| -> Value { serde_json::from_str(&document.to_json()).unwrap() };
// A flange that follows the rule saves neither key, and a design of format 23 opens as it was.
@@ -0,0 +1,6 @@
//! An edge flange feature's `position` can also be `material_outside` (the inner virtual sharp on
//! the picked edge, so at 90° the flange's inner face is flush with the edge's face) or `tangent`
//! (the bend's outer face tangent to the edge's face; below 90° the bend's end on that face),
//! #5941. Older designs need no step: their
//! positions (inside, outside, middle) are saved and built as before. The last release refuses a
//! design that has them (it refuses the newer format number).
@@ -0,0 +1,6 @@
//! An edge flange feature can say where its heights are measured from (#5938): `height_from` is
//! `inner` (the inner virtual sharp) or `tangent` (past 90° the line square to the flange
//! touching the outside of the bend, up to 90° the outer virtual sharp). A feature measured from the outer
//! virtual sharp, as every flange was before, saves no `height_from`, so older designs need no
//! step and open and save as they were. The last release refuses a design that has it (it
//! refuses the newer format number).
@@ -0,0 +1,5 @@
//! Text parameters (`text_params`: name, text, comment) and the expression behind a sketch
//! text's words (`exprs.text` of a text link, e.g. `Label + '-L'`), whose words are what it gave
//! last. Older designs need no transformation: they have neither. The last release refuses the
//! newer format number instead of opening a design whose texts would lose their link to the
//! parameters.
+105 -4
View File
@@ -13,6 +13,10 @@
//! around them tells apart are numbered (`#1`, `#2`): in the order of what they came from when
//! that differs (exact), else in the kernel's order (unverified). A face made of several (a
//! coplanar join) takes one name and answers to the others (aliases).
//!
//! A piece that is split again keeps the face's stem and the bounds that told the piece it
//! came from apart: its name lists the faces around it, not the qualifiers of the pieces
//! before it, so a name does not grow with every split.
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, OnceLock};
@@ -244,7 +248,7 @@ fn merged(names: Vec<Named>) -> Option<Named> {
}
common = crate::naming::parent_piece(c);
}
let name = common.map(str::to_string).unwrap_or(first);
let name = common_piece(&names).or_else(|| common.map(str::to_string)).unwrap_or(first);
let mut aliases: Vec<String> = Vec::new();
for n in names {
for alias in std::iter::once(n.name).chain(n.aliases) {
@@ -256,6 +260,38 @@ fn merged(names: Vec<Named>) -> Option<Named> {
Some(Named { name, aliases, verified, origins: None })
}
/// The name the faces have in common: their stem, the faces their qualifiers share and the
/// number that told their pieces apart, when all of them have it. A face made of pieces that
/// were split again keeps the name of the piece they came from (#5860).
fn common_piece(names: &[Named]) -> Option<String> {
let first = names.first()?.name.as_str();
let (stem, around, tie) = split_piece(first);
let mut shared: BTreeSet<String> = around.into_iter().collect();
let mut same_tie = tie;
for n in names.iter().skip(1) {
let (other_stem, around, other_tie) = split_piece(&n.name);
if other_stem != stem {
return None;
}
let around: BTreeSet<String> = around.into_iter().collect();
shared = shared.intersection(&around).cloned().collect();
if same_tie != other_tie {
same_tie = None;
}
}
let mut name = stem;
if !shared.is_empty() {
name.push_str("#[");
name.push_str(&shared.into_iter().collect::<Vec<_>>().join(","));
name.push(']');
}
if let Some(tie) = same_tie {
name.push('#');
name.push_str(&tie);
}
Some(name)
}
/// Whether `name` is `stem` or a piece of it (at any depth).
pub(crate) fn descends(name: &str, stem: &str) -> bool {
let mut current = Some(name);
@@ -330,11 +366,26 @@ fn qualify_at(named: Vec<Named>, adjacent: &[(usize, usize)], centres: Option<&d
})
.unwrap_or_default();
let own = |i: usize| -> Vec<&String> { bounds.get(i).unwrap_or(&empty).iter().filter(|x| !common.contains(x)).collect() };
// The face's stem, the faces the parent's qualifier lists (the bounds that told the
// parent apart from its siblings) and the number that told apart the pieces nothing
// else told apart.
let (stem, inherited, tie) = split_piece(base);
let mut by_key: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for &i in members {
let key: Vec<&str> = own(i).into_iter().map(String::as_str).collect();
// Nothing around it of its own: the piece is only numbered (below).
let name = if key.is_empty() { base.to_string() } else { format!("{base}#[{}]", key.join(",")) };
let own = own(i);
// Nothing around it of its own: the piece keeps the name it had.
let name = if own.is_empty() {
base.to_string()
} else {
// The bounds that told the parent apart and the bounds that tell this piece
// apart: the name says which piece of the face it is without repeating the
// qualifiers of the pieces before it (the name grew with every split
// otherwise, #5860).
let mut key: BTreeSet<&str> = inherited.iter().map(String::as_str).collect();
key.extend(own.iter().map(|s| s.as_str()));
let key: Vec<&str> = key.into_iter().collect();
piece_name(&stem, &key, tie.as_deref())
};
by_key.entry(name).or_default().push(i);
}
for (key, tied) in by_key {
@@ -394,6 +445,56 @@ fn qualify_at(named: Vec<Named>, adjacent: &[(usize, usize)], centres: Option<&d
Names { faces: Arc::new(names), info }
}
/// The most bytes a face name may take: a command takes a face name of at most this many
/// bytes, so a longer name could not be used to find the face again.
const MAX_NAME_BYTES: usize = 512;
/// A piece's name: the face's stem, the faces around the piece that tell it apart, as many
/// of them as the name can hold (`MAX_NAME_BYTES`), and the number that told the pieces of
/// the same face apart.
fn piece_name(stem: &str, faces: &[&str], tie: Option<&str>) -> String {
let mut kept: Vec<&str> = Vec::with_capacity(faces.len());
// The stem, the `#[` and the `]` the qualifier is written in.
let mut len = stem.len() + 3;
for face in faces {
let more = face.len() + if kept.is_empty() { 0 } else { 1 };
if len + more > MAX_NAME_BYTES {
break;
}
len += more;
kept.push(face);
}
let mut name = if kept.is_empty() { stem.to_string() } else { format!("{stem}#[{}]", kept.join(",")) };
if let Some(tie) = tie {
name.push('#');
name.push_str(tie);
}
name
}
/// A piece name's parts: the face's stem, the faces its qualifier lists and the number that
/// tells apart the pieces of the same face that nothing else tells apart
/// (`F2:top#[F3:side]#2`). A name that cannot be read is taken as it is: a hostile name never
/// panics.
fn split_piece(name: &str) -> (String, Vec<String>, Option<String>) {
let (stem, around) = match crate::piece_names::piece_parts(name) {
Some((stem, around)) => (stem.to_string(), around.iter().map(|face| face.to_string()).collect()),
None => (name.to_string(), Vec::new()),
};
(stem, around, tie_number(name))
}
/// The number at the end of a piece name (`#2` of `F2:top#[F3:side]#2`), when it has one: it
/// tells apart the pieces of a face that nothing else tells apart.
fn tie_number(name: &str) -> Option<String> {
let i = name.rfind('#')?;
let digits = name.get(i + 1..)?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some(digits.to_string())
}
/// `origin` with the face or edge it names left out (its kind and operand kept).
fn without_index(origin: FaceOrigin) -> FaceOrigin {
match origin {
+32
View File
@@ -79,6 +79,38 @@ fn pieces_nothing_tells_apart_are_numbered_and_unverified() {
assert!(!names.verified(0) && !names.verified(1));
}
/// #5860: a piece of a split face that is split again must not stack another qualifier on
/// its name: the name grew with every split (728 characters after eleven splits).
#[test]
fn a_piece_split_again_does_not_grow_its_name() {
// A face split in two: the pieces are told apart by the walls only they touch.
let first = qualify(named(&["top", "top", "wall:a", "wall:b"]), &[(0, 2), (1, 3)]);
assert_eq!(first.faces[0], "top#[wall:a]");
assert_eq!(first.faces[1], "top#[wall:b]");
// The piece that touches wall:a is split again, and again: its name lists the walls
// around it, not the qualifiers of the pieces it came from.
let mut piece = first.faces[0].clone();
for _ in 0..10 {
let names = qualify(named(&[piece.as_str(), piece.as_str(), "wall:a", "wall:b"]), &[(0, 2), (1, 3)]);
piece = names.faces[0].clone();
}
assert_eq!(piece, "top#[wall:a]");
}
/// A name has to stay within the length a command accepts for a face name (512 bytes), or
/// the face could not be named again.
#[test]
fn a_piece_with_many_faces_around_it_keeps_a_name_that_can_be_used() {
let around: Vec<String> = (0..80).map(|i| format!("F{i}:blend:face:{i}")).collect();
let mut faces: Vec<&str> = vec!["top", "top"];
faces.extend(around.iter().map(String::as_str));
// Each piece touches forty faces of its own: none of them tells the pieces apart.
let adjacent: Vec<(usize, usize)> = (2..42).map(|i| (0, i)).chain((42..82).map(|i| (1, i))).collect();
let names = qualify(named(&faces), &adjacent);
assert!(names.faces.iter().all(|n| n.len() <= 512), "{:?}", names.faces);
assert_ne!(names.faces[0], names.faces[1], "the pieces are still told apart");
}
#[test]
fn a_merged_face_takes_the_shared_stem_or_the_first_name_and_answers_to_the_others() {
let m = merged(named(&["top#[a]", "top#[b]"])).unwrap();
+50 -3
View File
@@ -2015,19 +2015,37 @@ impl Document {
/// Apply a solution: occurrence transforms (in their parents) for the occurrences it places,
/// and the values of the joints that moved to close loops. Returns true when something moved.
pub fn apply_joint_solution(&mut self, sol: &Solution) -> bool {
// Joints and occurrences are indexed once (the first of each id, as a search finds):
// searching them for each of a large design's thousands of values and occurrences took
// 20 ms on every change to the design.
let mut joint_at = std::collections::HashMap::new();
for (i, j) in self.assembly.joints.iter().enumerate() {
joint_at.entry(j.id).or_insert(i);
}
let mut changed = false;
for (id, q) in &sol.values {
if let Some(j) = self.assembly.joints.iter_mut().find(|j| j.id == *id) {
if let Some(j) = joint_at.get(id).and_then(|&i| self.assembly.joints.get_mut(i)) {
j.values.clone_from(q);
changed = true;
}
}
let parents: BTreeMap<u64, u64> = self.occurrences.iter().map(|o| (o.id, o.parent)).collect();
let (mut occurrence_at, mut first_of) = (std::collections::HashMap::new(), std::collections::HashMap::new());
for (i, o) in self.occurrences.iter().enumerate() {
occurrence_at.entry(o.id).or_insert(i);
first_of.entry(o.component).or_insert(i);
}
for (occ, world) in &sol.world {
let parent = parents.get(occ).copied().unwrap_or(0);
let pw = if parent == 0 { IDENTITY } else { self.component_transform(parent) };
// Through the transforms as this loop has left them, as `component_transform` reads.
let occurrences = &self.occurrences;
let pw = if parent == 0 {
IDENTITY
} else {
crate::assembly::transform_through(parent, |c| first_of.get(&c).and_then(|&i| occurrences.get(i)))
};
let Some(local) = mat_inverse(&pw).map(|inv| mat_mul(&inv, world)) else { continue };
if let Some(o) = self.occurrences.iter_mut().find(|o| o.id == *occ) {
if let Some(o) = occurrence_at.get(occ).and_then(|&i| self.occurrences.get_mut(i)) {
let (dt, da) = mismatch(&o.transform, &local);
if dt > 1e-9 || da > 1e-10 {
o.transform = local;
@@ -2393,4 +2411,33 @@ mod tests {
let (_, a) = mismatch(&rot_z(3.0), &IDENTITY);
assert!((a - 3.0).abs() < 1e-12, "{a}");
}
/// Applying a solution stores each occurrence in its parent's frame as the loop has left it:
/// a component the same solution moves is read moved, through its first occurrence. Joint
/// values land on their joints; a value for a missing joint is skipped.
#[test]
fn a_solution_places_occurrences_in_their_moved_parents() {
let occurrence =
|id, component, parent| crate::Occurrence { id, component, parent, name: format!("O{id}"), transform: IDENTITY, grounded: false };
// Component 1 sits at the root twice (its first occurrence is the frame its children sit
// in), component 2 inside 1 and component 3 inside 2.
let mut doc = Document {
assembly: chain(2, 0.0),
occurrences: vec![occurrence(10, 1, 0), occurrence(11, 1, 0), occurrence(20, 2, 1), occurrence(30, 3, 2)],
..Document::default()
};
let w10 = mat_mul(&shift(3.0, 4.0, 5.0), &rot_y(0.3));
let w20 = mat_mul(&shift(-1.0, 2.0, 0.5), &rot_x(-0.2));
let w30 = mat_mul(&shift(0.0, 7.0, 1.0), &rot_y(1.1));
let world = BTreeMap::from([(10, w10), (20, w20), (30, w30)]);
let sol = Solution { world: world.clone(), values: BTreeMap::from([(2, vec![0.75]), (99, vec![1.0])]), ..Solution::default() };
assert!(doc.apply_joint_solution(&sol));
for (component, w) in [(1, w10), (2, w20), (3, w30)] {
assert!(close(&doc.component_transform(component), &w), "component {component}");
}
assert!(close(&doc.occurrences[1].transform, &IDENTITY), "only the occurrence the solution names moves");
let values: Vec<_> = doc.assembly.joints.iter().map(|j| j.values.clone()).collect();
assert_eq!(values, vec![vec![0.0], vec![0.75]]);
assert!(!doc.apply_joint_solution(&Solution { world, ..Solution::default() }), "applied twice, nothing moves");
}
}
+1
View File
@@ -65,6 +65,7 @@ pub mod render_settings;
pub mod render_surface;
pub use project3d::iso_grid;
pub mod sheet;
pub mod text_params;
pub use assembly::{
IDENTITY, MAX_PLACEMENT_DEPTH, MAX_PLACEMENTS, Mat, Occurrence, apply_point, apply_vector, is_identity, map_point_expr, mat_inverse, mat_mul,
+1 -1
View File
@@ -636,7 +636,7 @@ impl Model {
let mut before = Arc::new(ModelState::default());
if let Ok(mut cache) = model.cache.lock() {
for (f, r) in doc.features.iter().zip(&model.results).filter(|(_, r)| !r.skipped && r.error.is_none()) {
let local = fingerprint(0, f, &vals, false, &model.step_digests) ^ placement_digest(doc, f, Some(&before));
let local = fingerprint(doc, 0, f, &vals, false, &model.step_digests) ^ placement_digest(doc, f, Some(&before));
cache.put(
cache_key(f, r.fingerprint, local, ctx, &before),
CachedEval {
+63 -2
View File
@@ -103,6 +103,9 @@ pub struct ParamRow {
/// Names this one's expression uses, and names whose expressions use this one.
pub uses: Vec<String>,
pub used_by: Vec<String>,
/// A text parameter's text (its unit is `text`, its value none).
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
fn unit_of(k: Kind) -> &'static str {
@@ -129,7 +132,7 @@ impl Document {
}
pub(crate) fn name_taken(&self, n: &str) -> bool {
self.param(n).is_some() || self.features.iter().any(|f| f.param_names.iter().any(|x| x == n))
self.param(n).is_some() || self.text_param(n).is_some() || self.features.iter().any(|f| f.param_names.iter().any(|x| x == n))
}
/// The next free `dN` name.
@@ -289,6 +292,14 @@ impl Document {
if expression.len() > 4096 || expression.trim().is_empty() {
return Err(DocError::Expr("expression is empty or too long".into()));
}
if let Some(p) = next.text_params.iter_mut().find(|p| p.name == *name) {
let text = crate::text_params::unquote(expression);
if p.text != text {
changed.insert(name.clone());
}
p.text = text;
continue;
}
if let Some(p) = next.params.iter_mut().find(|p| p.name == *name) {
if p.expr != expression.trim() {
changed.insert(name.clone());
@@ -349,6 +360,7 @@ impl Document {
favorite: self.favorites.contains(&p.name),
uses: refs.get(&p.name).cloned().unwrap_or_default(),
used_by: used_by(&p.name),
text: None,
});
}
for f in &self.features {
@@ -369,9 +381,28 @@ impl Document {
favorite: self.favorites.contains(name),
uses: refs.get(name).cloned().unwrap_or_default(),
used_by: used_by(name),
text: None,
});
}
}
for p in &self.text_params {
let readers = self.features_reading_texts(&BTreeSet::from([p.name.clone()]));
rows.push(ParamRow {
name: p.name.clone(),
expression: p.text.clone(),
unit: crate::text_params::TEXT_UNIT.into(),
value: None,
error: None,
comment: p.comment.clone(),
source: "user",
feature: None,
input: None,
favorite: self.favorites.contains(&p.name),
uses: Vec::new(),
used_by: readers.iter().filter_map(|id| self.feature(*id)).map(|f| format!("Feature:{}", f.name)).collect(),
text: Some(p.text.clone()),
});
}
rows
}
@@ -498,6 +529,9 @@ impl Document {
}
return Ok(());
}
if unit.map_or(self.text_param(name).is_some(), crate::text_params::is_text_unit) {
return self.set_text_param(name, expr_s, comment);
}
if let Some(p) = Self::param_name_problem(name) {
return Err(DocError::Invalid(p));
}
@@ -709,6 +743,9 @@ impl Document {
out.push(ParamUser::Feature { feature: f.id, name: f.name.clone(), input: what.to_string() });
}
}
if sketch.text_word_exprs().iter().any(|e| crate::text_params::references(e).iter().any(|r| r == name)) {
out.push(ParamUser::Feature { feature: f.id, name: f.name.clone(), input: "Text".into() });
}
}
}
for (joint, input, e) in self.assembly.exprs() {
@@ -740,6 +777,9 @@ impl Document {
/// Remove a stored parameter; refused while something uses it (the error lists the users).
pub fn remove_param(&mut self, name: &str) -> Result<()> {
if self.text_param(name).is_some() {
return self.remove_text_param(name);
}
if self.param(name).is_none() {
if self.features.iter().any(|f| f.param_names.iter().any(|n| n == name)) {
return Err(DocError::Invalid(format!("`{name}` is a feature's input: delete or edit the feature instead")));
@@ -774,6 +814,12 @@ impl Document {
return Err(DocError::Invalid(format!("there is already a parameter `{new}`")));
}
let mut n = 0;
let text = self.text_param(old).is_some();
for p in &mut self.text_params {
if p.name == old {
p.name = new.to_string();
}
}
for p in &mut self.params {
if p.name == old {
p.name = new.to_string();
@@ -825,6 +871,13 @@ impl Document {
n += 1;
}
}
for e in sketch.text_word_exprs_mut() {
let r = crate::text_params::rename_reference(e, old, new);
if r != *e {
*e = r;
n += 1;
}
}
}
}
if self.favorites.remove(old) {
@@ -839,9 +892,11 @@ impl Document {
for rule in &mut self.configs.rules {
rule.rename_parameter(old, new);
}
// A text parameter's cells are texts, not expressions.
let texts: BTreeSet<String> = self.text_params.iter().map(|p| p.name.clone()).collect();
for r in &mut self.configs.rows {
for (column, cell) in self.configs.columns.iter().zip(&mut r.cells) {
if matches!(column, crate::config::Column::Param(_)) {
if !text && matches!(column, crate::config::Column::Param(c) if !texts.contains(c)) {
*cell = expr::rename_reference(cell, old, new);
}
}
@@ -886,6 +941,10 @@ impl Document {
p.comment = comment.to_string();
return Ok(());
}
if let Some(p) = self.text_params.iter_mut().find(|p| p.name == name) {
p.comment = comment.to_string();
return Ok(());
}
if !self.name_taken(name) {
return Err(DocError::Unknown(format!("parameter `{name}`")));
}
@@ -907,6 +966,7 @@ impl Document {
names.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
names.extend(sketch.text_exprs().into_iter().flat_map(|(_, e)| expr::references(e)));
names.extend(sketch.patterns.iter().filter_map(|p| p.count_expression.as_deref()).flat_map(expr::references));
names.extend(sketch.text_word_exprs().into_iter().flat_map(|e| crate::text_params::references(e)));
}
// Sheet metal and plastic features rebuild when their rules' parameters change (#4420).
if f.kind.is_sheet() {
@@ -948,6 +1008,7 @@ impl Document {
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
hit |= sketch.constraints.iter().any(|c| c.param.as_ref().is_some_and(|p| affected.contains(p)));
hit |= sketch.text_exprs().iter().any(|(_, e)| expr::references(e).iter().any(|r| affected.contains(r)));
hit |= sketch.text_word_exprs().iter().any(|e| crate::text_params::references(e).iter().any(|r| affected.contains(r)));
}
if hit {
out.push(f.id);
+37
View File
@@ -21,6 +21,9 @@ pub struct Progress {
work_stage: std::sync::Mutex<&'static str>,
work_done: AtomicUsize,
work_total: AtomicUsize,
/// The item a command works through one at a time (the linked design Update Links reads
/// again): its name, its place and how many there are ([`item`]).
item: std::sync::Mutex<Option<(String, usize, usize)>>,
/// A window wants the model as far as it is built, to show while the rest is ([`Progress::want_partial`]).
partial_wanted: AtomicBool,
/// The latest such model and when it was offered.
@@ -48,6 +51,7 @@ impl Default for Progress {
work_stage: std::sync::Mutex::new(""),
work_done: AtomicUsize::new(0),
work_total: AtomicUsize::new(0),
item: std::sync::Mutex::new(None),
partial_wanted: AtomicBool::new(false),
partial: std::sync::Mutex::new((None, None)),
}
@@ -86,6 +90,23 @@ impl Progress {
let stage = self.work_stage.lock().map(|s| *s).unwrap_or("");
(total > 0 && !stage.is_empty()).then(|| (stage, self.work_done.load(Ordering::Relaxed).min(total), total))
}
/// The item being worked on, as [`item`] reported it: (name, index from 0, count).
pub fn item(&self) -> Option<(String, usize, usize)> {
self.item.lock().ok().and_then(|i| i.clone())
}
}
/// Tell a watcher which item a command is on (`name`, `index` from 0 of `count`), such as the
/// linked design Update Links reads again; `None` once it is past them. Nothing happens unless
/// the work is watched ([`watch`]).
pub fn item(current: Option<(&str, usize, usize)>) {
CURRENT.with(|c| {
if let Some(cur) = c.borrow().as_ref()
&& let Ok(mut i) = cur.progress.item.lock()
{
*i = current.map(|(name, index, count)| (name.to_string(), index.min(count), count));
}
});
}
/// The kernel's long work (STEP import) reports to the same progress and stops on Cancel.
@@ -242,6 +263,22 @@ mod tests {
assert!(!begin(1).step(0), "the watch ends with its scope");
}
/// The item a command is on reaches its watcher, and only a watcher.
#[test]
fn items_go_to_the_watcher() {
item(Some(("unwatched", 0, 1)));
let p = Arc::new(Progress::default());
assert_eq!(p.item(), None);
watch(p.clone(), || {
item(Some(("plate", 1, 3)));
assert_eq!(p.item(), Some(("plate".to_string(), 1, 3)));
item(None);
});
assert_eq!(p.item(), None, "cleared once past the items");
item(Some(("after", 0, 1)));
assert_eq!(p.item(), None, "the watch ends with its scope");
}
/// A watcher that asks for partial models gets the model built so far, placed, now and then;
/// one that doesn't ask, or an unwatched evaluation, never pays for it.
#[test]
+4
View File
@@ -688,7 +688,11 @@ fn refresh_links_in(
}
sk.set_link_source(&l.id, r.source);
let _ = sk.set_link_wires(&l.id, &r.wires);
// Words built from text parameters change shape when the parameters do: that
// rebuild is the text following them, not a reference that changed under it.
let words_follow = matches!(&l.source, LinkSource::Text { exprs, .. } if exprs.text.is_some());
match sk.update_link(&l.id, &r.geom) {
Ok(true) if words_follow => {}
Ok(true) => warn.push(format!("projected {} changed shape and was rebuilt", l.source.describe())),
Ok(false) if r.moved => warn.push(format!("projected {} was re-found on the nearest geometry", l.source.describe())),
Ok(false) => {}
+126 -25
View File
@@ -549,13 +549,75 @@ pub struct SheetFlange {
pub tag: String,
}
/// One edge flange to make: a pick on the sheet's edge, and that flange's height (to the outer
/// face, from the virtual sharp) and angle (radians).
/// One edge flange to make: a pick on the sheet's edge, that flange's height (measured as `from`
/// says) and angle (radians).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EdgeFlange {
pub pick: Vec3,
pub height: f64,
pub angle: f64,
pub from: HeightFrom,
}
/// Where an edge flange's height is measured from (#5938). Every one measures along the flange,
/// up to its far edge; they differ in where the measure starts, so in how much of the height is
/// the bend rather than the leg (the flat panel past the bend). With bend angle θ, inner radius
/// R and thickness T, the leg is the height less [`HeightFrom::before_leg`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum HeightFrom {
/// From the outer virtual sharp, where the lines of the base's and the flange's outer faces
/// meet, along the outer face: (R + T)·tan(θ/2) of the height is before the leg.
#[default]
Outer,
/// From the inner virtual sharp, where the lines of the two inner faces meet, along the inner
/// face: R·tan(θ/2) of the height is before the leg.
Inner,
/// For bends past 90°: from the line square to the flange that touches the outside of the
/// bend (at the point a quarter turn before the bend's end), R + T before the leg. Up to 90°
/// it is the outer virtual sharp, (R + T)·tan(θ/2); at 90° the two are the same line (the
/// base's outer face), so the height does not jump as the angle passes 90°.
Tangent,
}
impl HeightFrom {
pub const NAMES: [&'static str; 3] = ["outer", "inner", "tangent"];
pub fn parse(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"outer" => Some(Self::Outer),
"inner" => Some(Self::Inner),
"tangent" => Some(Self::Tangent),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Outer => "outer",
Self::Inner => "inner",
Self::Tangent => "tangent",
}
}
/// How much of the height lies before the leg starts, for a bend of `angle` (radians), inner
/// `radius` and `thickness`: the smallest height that fits (a flange with no leg).
pub fn before_leg(self, angle: f64, radius: f64, thickness: f64) -> f64 {
match self {
Self::Outer => (radius + thickness) * (angle / 2.0).tan(),
Self::Inner => radius * (angle / 2.0).tan(),
Self::Tangent if angle <= std::f64::consts::FRAC_PI_2 => (radius + thickness) * (angle / 2.0).tan(),
Self::Tangent => radius + thickness,
}
}
/// What the measure starts at, for messages.
fn datum(self) -> &'static str {
match self {
Self::Outer => "the outer sharp",
Self::Inner => "the inner sharp",
Self::Tangent => "the bend's tangent",
}
}
}
/// The most lines a path flange's path may have (each one flange and its bend).
@@ -2360,8 +2422,9 @@ impl SheetBody {
Ok(())
}
/// Add an edge flange on the panel edge nearest `pick` (world): `height` to the outer face
/// from the virtual sharp, `angle` (0…180°), inside / outside / middle bend position.
/// Add an edge flange on the panel edge nearest `pick` (world): `height` measured from the
/// outer virtual sharp ([`HeightFrom::Outer`]), `angle` (0…180°), inside / outside / middle /
/// material_outside / tangent bend position (see `add_flanges_each`).
pub fn add_flange(&mut self, pick: Vec3, height: f64, angle: f64, radius: f64, position: &str, flip: bool) -> Result<usize> {
self.add_flanges(&[pick], height, angle, radius, position, flip)?.into_iter().next().ok_or_else(|| DocError::Invalid("flange".into()))
}
@@ -2384,7 +2447,7 @@ impl SheetBody {
position: &str,
flip: bool,
) -> Result<(Vec<usize>, Vec<String>)> {
let each: Vec<EdgeFlange> = picks.iter().map(|&pick| EdgeFlange { pick, height, angle }).collect();
let each: Vec<EdgeFlange> = picks.iter().map(|&pick| EdgeFlange { pick, height, angle, from: HeightFrom::Outer }).collect();
self.add_flanges_each(&each, radius, position, flip, ReliefOverride::default())
}
@@ -2475,7 +2538,7 @@ impl SheetBody {
None => (pick, top == (turn < 0.0)),
Some(&i) => (self.far_edge_pick(i).ok_or_else(|| DocError::Invalid("the flange has no far edge to bend again".into()))?, turn < 0.0),
};
let each = [EdgeFlange { pick: at, height, angle: turn.abs() }];
let each = [EdgeFlange { pick: at, height, angle: turn.abs(), from: HeightFrom::Outer }];
let own = if k == 0 { relief } else { ReliefOverride::default() };
let (new, more) = self.add_flanges_each(&each, radius, "inside", flip, own).map_err(line)?;
made.extend(new);
@@ -2516,15 +2579,39 @@ impl SheetBody {
return Err(DocError::Invalid(format!("{}the flange angle must be between 0 and 180°", on(k))));
}
let sharp = (radius + self.t) * (e.angle / 2.0).tan();
// How far inside the picked edge the bend starts (the base is cut back that far), with
// bend angle θ, inner radius R and thickness T:
// - inside: (R + T)·tan(θ/2), the outer virtual sharp on the edge (at 90° the flange's
// outer face is flush with the edge's face);
// - outside: 0, the bend starts at the edge and lies wholly outside it (`sheet.flange`
// also takes `bend_outside` and saves it as `outside`);
// - middle: half of inside;
// - material_outside: R·tan(θ/2), the inner virtual sharp on the edge (at 90° the
// flange's inner face is flush with the edge's face, its material outside);
// - tangent: from 90° on R + T, the bend's outer face tangent to the edge's face (it
// touches that face a quarter turn into the bend and nothing of the bend passes it);
// below 90° the outer face never turns parallel to the edge's face, so the bend
// reaches furthest out at its end: (R + T)·sin θ puts that end on the edge's face
// (the leg leans on out past it). Both are R + T at 90°.
let setback = match position {
"inside" | "" => sharp,
"outside" => 0.0,
"middle" | "mid" => sharp / 2.0,
other => return Err(DocError::Invalid(format!("unknown bend position `{other}` (inside, outside or middle)"))),
"material_outside" => radius * (e.angle / 2.0).tan(),
"tangent" if e.angle < std::f64::consts::FRAC_PI_2 => (radius + self.t) * e.angle.sin(),
"tangent" => radius + self.t,
other => {
return Err(DocError::Invalid(format!("unknown bend position `{other}` (inside, outside, middle, material_outside or tangent)")));
}
};
let leg = e.height - sharp;
let least = e.from.before_leg(e.angle, radius, self.t);
let leg = e.height - least;
if !(leg >= -1e-9) {
return Err(DocError::Invalid(format!("{}the flange height must be at least {sharp:.3} mm for this bend", on(k))));
let from = match e.from {
HeightFrom::Outer => String::new(),
other => format!(" from {}", other.datum()),
};
return Err(DocError::Invalid(format!("{}the flange height{from} must be at least {least:.3} mm for this bend", on(k))));
}
sizes.push((setback, leg));
}
@@ -5986,7 +6073,8 @@ mod tests {
Vec3::new(60.0 + u.x * 90.0, u.y * 90.0, 2.0),
Vec3::new(u.x * 45.0, u.y * 45.0, 2.0),
];
let each: Vec<EdgeFlange> = (0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians() }).collect();
let each: Vec<EdgeFlange> =
(0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians(), from: HeightFrom::Outer }).collect();
flanges_each(rhombus(), rv, &each)
}
@@ -6016,7 +6104,7 @@ mod tests {
Vec3::new(l * 0.75, -l * u.y / 2.0, 2.0),
Vec3::new(l / 4.0, -l * u.y / 2.0, 2.0),
];
let flange = |i: usize| EdgeFlange { pick: picks[i], height: 38.6, angle: 90f64.to_radians() };
let flange = |i: usize| EdgeFlange { pick: picks[i], height: 38.6, angle: 90f64.to_radians(), from: HeightFrom::Outer };
// The round relief reaches r from its corner and the two are 12 mm apart: it fits at both.
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
let what = format!("{shape:?} at two corners opposite each other");
@@ -6167,7 +6255,7 @@ mod tests {
let pts = [Vec2::new(0.0, 0.0), Vec2::new(9.0, 0.0), Vec2::new(9.0, 40.0), Vec2::new(-20.0, 20.0 * 3.0f64.sqrt())];
let base = || Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] };
let picks = [Vec3::new(-10.0, 17.32, 2.0), Vec3::new(4.5, 0.0, 2.0), Vec3::new(9.0, 20.0, 2.0)];
let flange = |i: usize| EdgeFlange { pick: picks[i], height: 38.6, angle: 90f64.to_radians() };
let flange = |i: usize| EdgeFlange { pick: picks[i], height: 38.6, angle: 90f64.to_radians(), from: HeightFrom::Outer };
for shape in [ReliefShape::Round, ReliefShape::Rectangular] {
let what = format!("{shape:?} at both ends of a short edge");
let rv = RuleValues { corner_relief_shape: shape, corner_relief: 4.0, ..alu() };
@@ -6260,6 +6348,16 @@ mod tests {
holds_the_flat_material(&s, "a flange on a folded panel's side");
}
#[test]
fn a_bend_position_only_the_command_takes_as_another_name_is_refused_when_built() {
// `sheet.flange` saves `bend_outside` as `outside`; a design that names it itself is
// refused rather than built (or edited) as some other position.
let mut s = plate(200.0, 100.0);
let e = s.add_flange(Vec3::new(100.0, 0.0, 2.5), 40.0, std::f64::consts::FRAC_PI_2, 2.5, "bend_outside", false).unwrap_err().to_string();
assert!(e.contains("unknown bend position `bend_outside`"), "{e}");
assert!(s.flanges.is_empty());
}
#[test]
fn edge_flanges_across_an_existing_bend_close_their_corner() {
for position in ["inside", "outside", "middle"] {
@@ -6341,7 +6439,7 @@ mod tests {
let parent = s.flanges.get(wall).unwrap();
let point = parent.p + parent.d * parent.len + parent.n * (s.allowance(parent) + parent.leg * 0.5);
let side = apply_point(&s.world(Some(wall)), Vec3::new(point.x, point.y, s.t));
let edges = [Vec3::new(120.0, 45.0, 2.0), side].map(|pick| EdgeFlange { pick, height: 20.0, angle });
let edges = [Vec3::new(120.0, 45.0, 2.0), side].map(|pick| EdgeFlange { pick, height: 20.0, angle, from: HeightFrom::Outer });
let (_, notes) = s.add_flanges_each(&edges, 1.0, "inside", false, ReliefOverride { shape: Some(shape), size: Some(4.0) }).unwrap();
assert!(notes.is_empty(), "{shape:?}: {notes:?}");
assert_eq!(s.holes.len(), 1);
@@ -7059,7 +7157,8 @@ mod tests {
let base = rect(Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(120.0, 90.0), Vec2::new(0.0, 90.0));
let t = rv.thickness;
let picks = [Vec3::new(60.0, 0.0, t), Vec3::new(120.0, 45.0, t), Vec3::new(60.0, 90.0, t), Vec3::new(0.0, 45.0, t)];
let each: Vec<EdgeFlange> = (0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians() }).collect();
let each: Vec<EdgeFlange> =
(0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians(), from: HeightFrom::Outer }).collect();
flanges_each(base, rv, &each)
}
@@ -7259,7 +7358,8 @@ mod tests {
let pts = [Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(100.0, 90.0), Vec2::new(20.0, 90.0)];
let picks = [Vec3::new(60.0, 0.0, 2.0), Vec3::new(110.0, 45.0, 2.0), Vec3::new(60.0, 90.0, 2.0), Vec3::new(10.0, 45.0, 2.0)];
let (angles, heights) = ([60.0f64, 105.0, 75.0, 120.0], [38.6, 30.0, 45.0, 25.0]);
let each: Vec<EdgeFlange> = (0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians() }).collect();
let each: Vec<EdgeFlange> =
(0..4).map(|i| EdgeFlange { pick: picks[i], height: heights[i], angle: angles[i].to_radians(), from: HeightFrom::Outer }).collect();
let (s, notes) = flanges_each(Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] }, &alu(), &each);
assert!(notes.is_empty(), "{notes:?}");
for i in 0..4 {
@@ -7283,7 +7383,7 @@ mod tests {
.iter()
.map(|&k| {
let m = (pts[k] + pts[(k + 1) % n]) * 0.5;
EdgeFlange { pick: Vec3::new(m.x, m.y, rv.thickness), height, angle: deg.to_radians() }
EdgeFlange { pick: Vec3::new(m.x, m.y, rv.thickness), height, angle: deg.to_radians(), from: HeightFrom::Outer }
})
.collect();
let (s, notes) = flanges_each(Region2 { outer: Loop2::polygon(pts).ccw(), holes: vec![] }, rv, &each);
@@ -7602,8 +7702,9 @@ mod tests {
let rv = alu();
let (t, g) = (rv.thickness, rv.gap);
let (mut s, _) = tray(&rv, 90.0, 20.0);
let each: Vec<EdgeFlange> =
(0..4).map(|i| EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: [90f64, 70.0][i % 2].to_radians() }).collect();
let each: Vec<EdgeFlange> = (0..4)
.map(|i| EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: [90f64, 70.0][i % 2].to_radians(), from: HeightFrom::Outer })
.collect();
let (made, notes) = s.add_flanges_each(&each, 1.0, "inside", false, ReliefOverride::default()).unwrap();
assert!(notes.is_empty() && made == [4, 5, 6, 7], "{made:?} {notes:?}");
for (a, a_end, b, b_end) in TRAY_RETURNS {
@@ -7774,7 +7875,7 @@ mod tests {
let each: Vec<EdgeFlange> = (0..4)
.map(|i| {
let deg: f64 = if square { 90.0 } else { 180.0 - angles[i] };
EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: deg.to_radians() }
EdgeFlange { pick: far_edge_pick(&s, i, true), height: 12.0, angle: deg.to_radians(), from: HeightFrom::Outer }
})
.collect();
let (_, notes) = s.add_flanges_each(&each, 1.0, "inside", false, ReliefOverride::default()).unwrap();
@@ -7914,8 +8015,8 @@ mod tests {
for b in SWEEP {
let what = format!("corner {alpha}°, flanges {a}° and {b}°");
let each = [
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians() },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians() },
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians(), from: HeightFrom::Outer },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians(), from: HeightFrom::Outer },
];
let (s, notes) = flanges_each(Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] }, &rv, &each);
assert!(notes.is_empty(), "{what}: {notes:?}");
@@ -8314,8 +8415,8 @@ mod tests {
for b in SWEEP {
let what = format!("corner {alpha}°, flanges {a}° and {b}°");
let each = [
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians() },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians() },
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians(), from: HeightFrom::Outer },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians(), from: HeightFrom::Outer },
];
let base = || Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] };
let (notch, _) = flanges_each(base(), &alu(), &each);
@@ -8482,8 +8583,8 @@ mod tests {
let u = Vec2::new(alpha.cos(), alpha.sin());
let pts = [Vec2::new(0.0, 0.0), Vec2::new(120.0, 0.0), Vec2::new(120.0, 0.0) + u * 90.0, u * 90.0];
let each = [
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians() },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians() },
EdgeFlange { pick: Vec3::new(60.0, 0.0, 2.0), height: 38.6, angle: a.to_radians(), from: HeightFrom::Outer },
EdgeFlange { pick: Vec3::new(u.x * 45.0, u.y * 45.0, 2.0), height: 38.6, angle: b.to_radians(), from: HeightFrom::Outer },
];
let base = || Region2 { outer: Loop2::polygon(&pts).ccw(), holes: vec![] };
let (notch, none) = flanges_each(base(), &alu(), &each);
+50
View File
@@ -492,3 +492,53 @@ fn new_names_take_the_first_free_number() {
let next = sketch(&mut doc);
assert_eq!(name(&doc, next), names[0], "the first freed number is taken again");
}
#[test]
fn empty_boolean_targets_name_overlapping_component() {
for (operation, offset) in [(Operation::Cut, 0.0), (Operation::Intersect, 0.0), (Operation::Cut, 40.0)] {
let (mut doc, sketch) = plate_doc();
let imported = doc.add_component(Some("Imported STEP"), 0).unwrap();
let part = doc.add_component(Some("Part1"), 0).unwrap();
doc.features.last_mut().unwrap().component = imported;
doc.feature_mut(sketch).unwrap().component = imported;
doc.occurrences.iter_mut().find(|o| o.component == imported).unwrap().transform[2][3] = offset;
let mut kind = doc.features.last().unwrap().kind.clone();
if let FeatureKind::Extrude { operation: op, .. } = &mut kind {
*op = operation;
}
let cut = doc.add_feature(kind, None).unwrap();
doc.feature_mut(cut).unwrap().component = part;
let mut model = Model::new();
model.evaluate(&doc);
let error = model.result(cut).unwrap().error.as_deref().unwrap();
assert!(error.contains("component 'Part1'"), "offset {offset}: {error}");
assert!(error.contains("'Body1' in component 'Imported STEP'"), "offset {offset}: {error}");
assert!(error.contains("activate that component or pick the body as a target"), "offset {offset}: {error}");
assert_eq!(model.state().bodies.len(), 1);
assert!(rel(measure(&model.state().bodies[0].body).unwrap().volume, 24000.0) < 1e-9);
}
}
#[test]
fn empty_boolean_targets_without_overlap_keep_plain_message() {
for operation in [Operation::Cut, Operation::Intersect] {
let (mut doc, _) = plate_doc();
let part = doc.add_component(Some("Part1"), 0).unwrap();
let cut = doc
.add_feature(
FeatureKind::Box {
corner: point_expr(Vec3::new(100.0, 100.0, 100.0)),
length: "1".into(),
width: "1".into(),
height: "1".into(),
operation,
},
None,
)
.unwrap();
doc.feature_mut(cut).unwrap().component = part;
let mut model = Model::new();
model.evaluate(&doc);
assert_eq!(model.result(cut).unwrap().error.as_deref(), Some("there is no body to cut or intersect"));
}
}
+252
View File
@@ -0,0 +1,252 @@
//! Text parameters: named texts (`Label` = `TAG-1`) that the words of a sketch text can be built
//! from, by an expression that joins them and quoted texts with `+` (`Label + '-L'`). They are
//! kept apart from the number parameters: a text has no unit and no number expression reads it.
//! A parameter edit or a configuration row that sets one relabels every text built from it, and
//! the features that use those texts follow in the same rebuild.
use serde::{Deserialize, Serialize};
use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result};
/// The unit that marks a text parameter where parameters are listed, added or imported.
pub const TEXT_UNIT: &str = "text";
/// Longest text a text parameter holds, and longest words expression.
const MAX_LEN: usize = 4096;
/// A named text.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TextParam {
pub name: String,
pub text: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub comment: String,
}
/// Is `unit` the text unit?
pub fn is_text_unit(unit: &str) -> bool {
unit.trim() == TEXT_UNIT
}
/// A text as typed for a text parameter: inside matching quotes, what they hold (a quote doubled
/// inside stands for one); else the text itself, without surrounding spaces.
pub fn unquote(s: &str) -> String {
let t = s.trim();
for q in ['\'', '"'] {
if t.len() >= 2 && t.starts_with(q) && t.ends_with(q) {
let inner = t.get(1..t.len() - 1).unwrap_or_default();
let doubled = format!("{q}{q}");
// Only a text whose inner quotes all come in pairs is one quoted text.
if inner.replace(&doubled, "").contains(q) {
break;
}
return inner.replace(&doubled, &q.to_string());
}
}
t.to_string()
}
enum Term {
Text(String),
Name(String),
}
/// The terms of a words expression with their byte ranges: quoted texts (single or double
/// quotes, a quote doubled inside stands for one) and parameter names, joined by `+`.
fn terms(s: &str) -> Result<Vec<(Term, usize, usize)>> {
if s.len() > MAX_LEN {
return Err(DocError::Expr("text expression too long".into()));
}
let cs: Vec<(usize, char)> = s.char_indices().collect();
let at = |i: usize| cs.get(i).map_or(s.len(), |x| x.0);
let mut out = Vec::new();
let mut i = 0;
let mut want_term = true;
while let Some(&(_, c)) = cs.get(i) {
if c.is_whitespace() {
i += 1;
} else if !want_term {
if c != '+' {
return Err(DocError::Expr(format!("unexpected `{c}`: a text expression joins quoted texts and text parameters with +")));
}
want_term = true;
i += 1;
} else if c == '\'' || c == '"' {
let start = i;
let mut text = String::new();
i += 1;
loop {
match cs.get(i).map(|x| x.1) {
None => return Err(DocError::Expr(format!("a text in quotes ({c}) is not closed"))),
Some(d) if d == c && cs.get(i + 1).is_some_and(|x| x.1 == c) => {
text.push(c);
i += 2;
}
Some(d) if d == c => {
i += 1;
break;
}
Some(d) => {
text.push(d);
i += 1;
}
}
}
out.push((Term::Text(text), at(start), at(i)));
want_term = false;
} else if c.is_alphabetic() || c == '_' {
let start = i;
while cs.get(i).is_some_and(|x| x.1.is_alphanumeric() || x.1 == '_') {
i += 1;
}
out.push((Term::Name(s.get(at(start)..at(i)).unwrap_or_default().to_string()), at(start), at(i)));
want_term = false;
} else {
return Err(DocError::Expr(format!("unexpected `{c}`: a text expression joins quoted texts and text parameters with +")));
}
}
if want_term {
let what = if out.is_empty() { "the text expression is empty" } else { "the text expression ends with +" };
return Err(DocError::Expr(what.into()));
}
Ok(out)
}
/// The parameters a words expression names (none when it does not parse).
pub fn references(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for (t, _, _) in terms(s).unwrap_or_default() {
if let Term::Name(n) = t
&& !out.contains(&n)
{
out.push(n);
}
}
out
}
/// The words expression with every reference to `old` renamed to `new` (quoted texts kept).
pub fn rename_reference(s: &str, old: &str, new: &str) -> String {
let Ok(terms) = terms(s) else { return s.to_string() };
let mut out = String::with_capacity(s.len());
let mut last = 0;
for (t, a, b) in terms {
if matches!(&t, Term::Name(n) if n == old) {
out.push_str(s.get(last..a).unwrap_or_default());
out.push_str(new);
last = b;
}
}
out.push_str(s.get(last..).unwrap_or_default());
out
}
/// The words an expression comes to, each parameter's text from `lookup`.
pub fn eval(s: &str, lookup: &dyn Fn(&str) -> Result<String>) -> Result<String> {
let mut out = String::new();
for (t, _, _) in terms(s)? {
match t {
Term::Text(text) => out.push_str(&text),
Term::Name(n) => out.push_str(&lookup(&n)?),
}
if out.len() > MAX_LEN {
return Err(DocError::Expr("the text is too long".into()));
}
}
Ok(out)
}
impl Document {
pub fn text_param(&self, name: &str) -> Option<&TextParam> {
self.text_params.iter().find(|p| p.name == name)
}
/// The words a words expression comes to in this design.
pub fn eval_words(&self, e: &str) -> Result<String> {
eval(e, &|n| match self.text_param(n) {
Some(p) => Ok(p.text.clone()),
None if self.name_taken(n) => Err(DocError::Expr(format!("`{n}` is a number parameter: a text's words take text parameters"))),
None => Err(DocError::Expr(format!("unknown text parameter `{n}`"))),
})
}
/// Set text parameter `name` to `text` (as typed: [`unquote`]), adding it when new.
pub fn set_text_param(&mut self, name: &str, text: &str, comment: Option<&str>) -> Result<()> {
if let Some(p) = Self::param_name_problem(name) {
return Err(DocError::Invalid(p));
}
if text.len() > MAX_LEN {
return Err(DocError::Expr("text too long".into()));
}
if comment.is_some_and(|c| c.len() > 4096) {
return Err(DocError::Invalid("comment too long".into()));
}
let text = unquote(text);
if let Some(p) = self.text_params.iter_mut().find(|p| p.name == name) {
p.text = text;
if let Some(c) = comment {
p.comment = c.to_string();
}
return Ok(());
}
if self.name_taken(name) {
return Err(DocError::Invalid(format!("`{name}` is a number parameter")));
}
if self.params.len() + self.text_params.len() >= MAX_PARAMS {
return Err(DocError::Invalid("too many parameters".into()));
}
self.text_params.push(TextParam { name: name.to_string(), text, comment: comment.unwrap_or_default().to_string() });
Ok(())
}
/// Remove text parameter `name`; refused while a text uses it.
pub(crate) fn remove_text_param(&mut self, name: &str) -> Result<()> {
let users = self.param_users(name);
if !users.is_empty() {
let list: Vec<String> = users.iter().map(crate::ParamUser::describe).collect();
return Err(DocError::Invalid(format!("`{name}` is used by {}", list.join(", "))));
}
self.text_params.retain(|p| p.name != name);
self.favorites.remove(name);
Ok(())
}
/// The sketches whose texts' words read one of `names`.
pub(crate) fn features_reading_texts(&self, names: &std::collections::BTreeSet<String>) -> Vec<u64> {
self.features
.iter()
.filter(|f| match &f.kind {
FeatureKind::Sketch { sketch, .. } => sketch.text_word_exprs().iter().any(|e| references(e).iter().any(|r| names.contains(r))),
_ => false,
})
.map(|f| f.id)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn words_expressions_join_quoted_texts_and_parameters() {
let look = |n: &str| match n {
"Label" => Ok("TAG-1".to_string()),
_ => Err(DocError::Expr(format!("unknown text parameter `{n}`"))),
};
assert_eq!(eval("Label + '-L'", &look).unwrap(), "TAG-1-L");
assert_eq!(eval("\"it's \" + Label", &look).unwrap(), "it's TAG-1");
assert_eq!(eval("'it''s'", &look).unwrap(), "it's");
assert_eq!(eval("'a+b' + 'c'", &look).unwrap(), "a+bc");
for bad in ["", "Label +", "+ 'a'", "Label 'a'", "'open", "Label - 'a'", "Other"] {
assert!(eval(bad, &look).is_err(), "{bad}");
}
assert_eq!(references("Label + 'Label' + Other + Label"), ["Label", "Other"]);
assert_eq!(rename_reference("Label+'Label-' + Label", "Label", "Tag"), "Tag+'Label-' + Tag");
assert_eq!(unquote(" 'A B' "), "A B");
assert_eq!(unquote("\"it''s\""), "it''s");
assert_eq!(unquote("'it''s'"), "it's");
assert_eq!(unquote("'a' + 'b'"), "'a' + 'b'");
assert_eq!(unquote("TAG-1"), "TAG-1");
}
}
+28 -20
View File
@@ -3,8 +3,10 @@
//! belong to their feature's component unless moved; occurrences place components (instances
//! share contents, "paste new" copies them) and can be grounded and moved.
use std::collections::HashMap;
use serde_json::{Value, json};
use solidcraft_doc::{FeatureKind, Mat, Occurrence};
use solidcraft_doc::{Feature, FeatureKind, Mat, Occurrence};
use solidcraft_geom::Vec3;
use super::CommandSpec;
@@ -40,10 +42,10 @@ pub static COMMANDS: &[CommandSpec] = &[
),
CommandSpec::new("component.properties", "Component Physical Properties", properties::component)
.icon("measure").noundo()
.params("component?: id or name (default active); descendants?: bool (default true). Measures the first placement, or all placements for root, including pending moves; material-aware mass and center of mass in world coordinates"),
.params("component?: id or name (default active); descendants?: bool (default true). Measures the first placement, or all placements for root, including pending moves; material-aware mass and center of mass in world coordinates; volume, density, mass and center of mass only for closed, consistently oriented bodies; total.open_bodies_skipped counts open bodies excluded from volume/mass totals"),
CommandSpec::new("occurrence.properties", "Occurrence Physical Properties", properties::occurrence)
.icon("measure").noundo()
.params("occurrence: id or name; path?: [occurrence ids] (required if nested under multiple instances); descendants?: bool (default true). Physical properties of this placement in world coordinates, including pending moves"),
.params("occurrence: id or name; path?: [occurrence ids] (required if nested under multiple instances); descendants?: bool (default true). Physical properties of this placement in world coordinates, including pending moves; volume, density, mass and center of mass only for closed, consistently oriented bodies; total.open_bodies_skipped counts open bodies excluded from volume/mass totals"),
CommandSpec::new("component.rename", "Rename Component", rename).icon("rename").params("component, name"),
CommandSpec::new("component.delete", "Delete Component", delete).icon("delete").params("component: deletes it, its subcomponents, occurrences and features"),
CommandSpec::new("component.unlock", "Unlock Contents", unlock).icon("rigid_group").params(
@@ -204,24 +206,30 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
}
v
};
// Each component's bodies, features, occurrences and children, grouped once rather than
// searched for every component (thousands of each in a design with many components).
let mut bodies_of: HashMap<u64, Vec<&str>> = HashMap::new();
for b in &st.bodies {
bodies_of.entry(doc.model_body_component(b)).or_default().push(b.name.as_str());
}
let mut features_of: HashMap<u64, Vec<&Feature>> = HashMap::new();
for f in &doc.features {
features_of.entry(f.component).or_default().push(f);
}
let (mut occurrences_of, mut children_of): (HashMap<u64, Vec<&Occurrence>>, HashMap<u64, Vec<&Occurrence>>) = Default::default();
for o in &doc.occurrences {
occurrences_of.entry(o.component).or_default().push(o);
children_of.entry(o.parent).or_default().push(o);
}
let node = |id: u64, name: &str, parent: Option<u64>| {
let bodies: Vec<&str> = st.bodies.iter().filter(|b| doc.model_body_component(b) == id).map(|b| b.name.as_str()).collect();
let features: Vec<&str> = doc
.features
.iter()
.filter(|f| f.component == id && !matches!(f.kind, FeatureKind::Sketch { .. } | FeatureKind::ConstructionPlane { .. }))
.map(|f| f.name.as_str())
.collect();
let sketches: Vec<&str> =
doc.features.iter().filter(|f| f.component == id && matches!(f.kind, FeatureKind::Sketch { .. })).map(|f| f.name.as_str()).collect();
let construction: Vec<&str> = doc
.features
.iter()
.filter(|f| f.component == id && matches!(f.kind, FeatureKind::ConstructionPlane { .. }))
.map(|f| f.name.as_str())
.collect();
let occurrences: Vec<Value> = doc.occurrences.iter().filter(|o| o.component == id).map(placement).collect();
let children: Vec<Value> = doc.occurrences.iter().filter(|o| o.parent == id).map(|o| if compact { json!(o.id) } else { json!(o) }).collect();
let bodies = bodies_of.get(&id).cloned().unwrap_or_default();
let features = features_of.get(&id).map(Vec::as_slice).unwrap_or_default();
let named = |keep: fn(&FeatureKind) -> bool| -> Vec<&str> { features.iter().filter(|f| keep(&f.kind)).map(|f| f.name.as_str()).collect() };
let sketches = named(|k| matches!(k, FeatureKind::Sketch { .. }));
let construction = named(|k| matches!(k, FeatureKind::ConstructionPlane { .. }));
let features = named(|k| !matches!(k, FeatureKind::Sketch { .. } | FeatureKind::ConstructionPlane { .. }));
let occurrences: Vec<Value> = occurrences_of.get(&id).into_iter().flatten().map(|o| placement(o)).collect();
let children: Vec<Value> = children_of.get(&id).into_iter().flatten().map(|o| if compact { json!(o.id) } else { json!(o) }).collect();
let bodies = if body_names { json!(bodies) } else { json!(bodies.len()) };
json!({"id": id, "name": name, "parent": parent, "bodies": bodies, "features": names(features), "sketches": names(sketches),
"construction": names(construction), "occurrences": occurrences, "children": children})
+5 -1
View File
@@ -233,13 +233,16 @@ fn update_links(s: &mut Session, p: &Value) -> Result<Value> {
}
let links: Vec<(u64, DesignLink)> = s.doc.links.iter().filter(|(c, _)| only.is_none_or(|o| o == **c)).map(|(c, l)| (*c, l.clone())).collect();
let (mut updated, mut unchanged, mut warnings) = (Vec::new(), Vec::new(), Vec::new());
for (comp, link) in links {
let count = links.len();
for (index, (comp, link)) in links.into_iter().enumerate() {
let cname = s.doc.components.iter().find(|c| c.id == comp).map(|c| c.name.clone());
let Some(cname) = cname else {
// The component was deleted: the link goes with it.
s.doc_mut().links.remove(&comp);
continue;
};
// A window running this off its thread names the design being read again (#5329).
solidcraft_doc::progress::item(Some((&cname, index, count)));
let Some(FeatureKind::Import { step: old, .. }) = s.doc.feature(link.feature).filter(|f| f.component == comp).map(|f| &f.kind) else {
return Err(bad(
cmd,
@@ -279,6 +282,7 @@ fn update_links(s: &mut Session, p: &Value) -> Result<Value> {
}
if changed { updated.push(cname) } else { unchanged.push(cname) }
}
solidcraft_doc::progress::item(None);
s.refresh();
if let Some(e) = s.doc.links.values().find_map(|l| s.model.result(l.feature).and_then(|r| r.error.clone())) {
return Err(EngineError::Other(e));
+13 -6
View File
@@ -93,6 +93,7 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
let mut bounds = Aabb3::EMPTY;
let (mut faces, mut edges, mut vertices) = (0, 0, 0);
let mut placement_count = 0;
let mut skipped = 0;
let selected_placements = if selected.is_empty() {
if descendants { placements(doc, cmd)? } else { vec![(0, Vec::new(), solidcraft_doc::IDENTITY)] }
} else {
@@ -121,18 +122,24 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
let measured = solidcraft_kernel::measure(&placed)?;
let density = doc.density(&body.name);
let body_mass = measured.volume * density / 1000.0;
volume += measured.volume;
let closed = crate::cmd::inspect::encloses_volume(&placed);
if closed {
volume += measured.volume;
mass += body_mass;
moment += measured.centroid * body_mass;
} else {
skipped += 1;
}
area += measured.area;
mass += body_mass;
moment += measured.centroid * body_mass;
bounds = bounds.union(&measured.bbox);
faces += measured.merged.faces;
edges += measured.merged.edges;
vertices += measured.merged.vertices;
bodies.push(json!({"name": body.name, "component": component, "path": path,
"material": doc.body_material(&body.name),
"density_g_cm3": density, "mass_g": body_mass, "volume_mm3": measured.volume,
"area_mm2": measured.area, "center_of_mass": measured.centroid,
"closed": closed, "density_g_cm3": closed.then_some(density),
"mass_g": closed.then_some(body_mass), "volume_mm3": closed.then_some(measured.volume),
"area_mm2": measured.area, "center_of_mass": closed.then_some(measured.centroid),
"bbox": {"min": measured.bbox.min, "max": measured.bbox.max}}));
}
}
@@ -146,7 +153,7 @@ fn properties(s: &Session, doc: &Document, selected: &[u64], descendants: bool,
let bbox = if bodies.is_empty() { Value::Null } else { json!({"min": bounds.min, "max": bounds.max}) };
Ok(json!({"path": selected, "coordinate_frame": "world", "descendants": descendants,
"placement_count": placement_count, "body_count": bodies.len(), "bodies": bodies,
"total": {"volume_mm3": volume, "area_mm2": area, "mass_g": mass,
"total": {"open_bodies_skipped": skipped, "volume_mm3": volume, "area_mm2": area, "mass_g": mass,
"center_of_mass": centroid, "bbox": bbox, "faces": faces, "edges": edges, "vertices": vertices}}))
}
+1 -1
View File
@@ -98,7 +98,7 @@ fn column_of(s: &Session, p: &Value, cmd: &str) -> Result<Column> {
}
}
if let Some(n) = str_(p, "param") {
if !s.doc.all_param_exprs().iter().any(|(m, _, _)| m == n) {
if !s.doc.all_param_exprs().iter().any(|(m, _, _)| m == n) && s.doc.text_param(n).is_none() {
return Err(bad(cmd, format!("no parameter `{n}`")));
}
return Ok(Column::Param(n.to_string()));
+1 -1
View File
@@ -88,7 +88,7 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
Sel::Occurrence { path } => {
selected_occurrences.push(s.occurrence_placement(path)?.occurrence.to_string());
}
Sel::SketchCurve { id } | Sel::SketchPoint { id } | Sel::SketchConstraint { id } => entities.push(id.clone()),
Sel::SketchCurve { id, .. } | Sel::SketchPoint { id } | Sel::SketchConstraint { id } => entities.push(id.clone()),
Sel::Feature { id } => {
if s.doc.feature(*id).is_none() {
return Err(bad(cmd, format!("no feature {id}")));
+71 -4
View File
@@ -26,8 +26,7 @@ pub(super) fn face_normal(s: &Session, p: Vec3) -> Option<Vec3> {
/// The outward normal of the planar face at `p` of `body` (any body when `None`).
pub(super) fn face_normal_in(s: &Session, p: Vec3, body: Option<&str>) -> Option<Vec3> {
let (m, f, d, _) = face_at_in(s, p, body).ok().flatten()?;
let tol = (m.bounds().diagonal() * 1e-3).max(1e-3);
if d > tol * 10.0 {
if !near_face(&m, d) {
return None;
}
m.triangles
@@ -38,11 +37,79 @@ pub(super) fn face_normal_in(s: &Session, p: Vec3, body: Option<&str>) -> Option
.find_map(|[a, b, c]| (b - a).cross(c - a).normalized())
}
/// Is a face `d` from a point near enough to hold it: within a thousandth of the body's
/// size (at least 1 µm), ten times over, as picks placed on faces are taken.
fn near_face(m: &Mesh, d: f64) -> bool {
let tol = (m.bounds().diagonal() * 1e-3).max(1e-3);
d <= tol * 10.0
}
/// Does `p` lie on a face of a body (planar or curved), as features placed on a face need?
/// Where flush bodies' faces are equally near, it lies on each of them. A point in the opening
/// of a planar face (the mouth of an existing bore, which a counterbore is centred on) is on
/// that face too.
pub(super) fn on_a_face(s: &Session, p: Vec3) -> bool {
let on_one = match face_at_in(s, p, None) {
Ok(Some((m, _, d, _))) => near_face(&m, d),
Ok(None) => false,
Err(tied) => tied.first().is_some_and(|b| matches!(face_at_in(s, p, Some(b)), Ok(Some((m, _, d, _))) if near_face(&m, d))),
};
on_one || s.model.state().bodies.iter().any(|b| in_a_planar_face_outline(&b.mesh(), p))
}
/// Does `p` lie in the plane of a planar face of `m`, inside the hull of that face's corners
/// (so over its openings, not past its edge)?
fn in_a_planar_face_outline(m: &Mesh, p: Vec3) -> bool {
let mut faces: std::collections::BTreeMap<u32, Vec<[Vec3; 3]>> = std::collections::BTreeMap::new();
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
if let Some(tri) = m.tri(t) {
faces.entry(*f).or_default().push(tri);
}
}
faces.values().any(|tris| {
let Some([a, b, c]) = tris.first().copied() else { return false };
let Some(n) = (b - a).cross(c - a).normalized() else { return false };
let planar = tris.iter().all(|[a, b, c]| (*b - *a).cross(*c - *a).normalized().is_some_and(|k| k.dot(n) > 1.0 - 1e-6));
if !planar || !near_face(m, (p - a).dot(n).abs()) {
return false;
}
let reference = if n.x.abs() < 0.9 { Vec3::new(1.0, 0.0, 0.0) } else { Vec3::new(0.0, 1.0, 0.0) };
let Some(u) = n.cross(reference).normalized() else { return false };
let v = n.cross(u);
let flat = |q: Vec3| (q.dot(u), q.dot(v));
let mut corners: Vec<(f64, f64)> = tris.iter().flatten().map(|q| flat(*q)).collect();
corners.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
corners.dedup();
inside_hull(&corners, flat(p))
})
}
/// Is `q` inside (or on) the convex hull of `sorted` points (sorted by x, then y)?
fn inside_hull(sorted: &[(f64, f64)], q: (f64, f64)) -> bool {
let cross = |o: (f64, f64), a: (f64, f64), b: (f64, f64)| (a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0);
let chain = |points: &mut dyn Iterator<Item = &(f64, f64)>| {
let mut h: Vec<(f64, f64)> = Vec::new();
for &pt in points {
while h.len() >= 2 && cross(h[h.len() - 2], h[h.len() - 1], pt) <= 0.0 {
h.pop();
}
h.push(pt);
}
h
};
let mut hull = chain(&mut sorted.iter());
hull.pop();
let mut upper = chain(&mut sorted.iter().rev());
upper.pop();
hull.extend(upper);
let eps = 1e-9;
hull.len() >= 3 && (0..hull.len()).all(|i| cross(hull[i], hull[(i + 1) % hull.len()], q) >= -eps)
}
/// Is the body face at `p` planar (`None`: no face there)?
pub(super) fn face_is_planar(s: &Session, p: Vec3) -> Option<bool> {
let (m, f, d, _) = face_at(s, p)?;
let tol = (m.bounds().diagonal() * 1e-3).max(1e-3);
if d > tol * 10.0 {
if !near_face(&m, d) {
return None;
}
let ns: Vec<Vec3> = m
+59
View File
@@ -1481,6 +1481,57 @@ fn combine(s: &mut Session, p: &Value) -> Result<Value> {
}
fn move_bodies(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(plan) = crate::frames::placements_moved(s, p) {
return move_placements(s, p, plan);
}
move_body_feature(s, p)
}
/// A move of several placements of one part (`frames::placements_moved`, #4768): each placement
/// moved in the world by the move's translation and turn (about the world origin, as the Move
/// feature turns bodies), and the root component's bodies listed moved as bodies, in one step.
fn move_placements(s: &mut Session, p: &Value, plan: crate::frames::PlacementMove) -> Result<Value> {
let cmd = "solid.move";
let length = |v: &Value| -> Result<f64> {
match v {
Value::Number(n) => n.as_f64().ok_or_else(|| bad(cmd, "translate entries must be numbers or expressions")),
Value::String(e) => s.doc.eval(e, Kind::Length).map_err(|e| bad(cmd, format!("translate: {e}"))),
_ => Err(bad(cmd, "translate entries must be numbers or expressions")),
}
};
let translate = match p.get("translate") {
Some(Value::Array(a)) if a.len() == 3 => a.iter().map(length).collect::<Result<Vec<f64>>>()?,
None => vec![0.0; 3],
_ => return Err(bad(cmd, "`translate` must be [x, y, z]")),
};
let angle = match expr(p, "angle") {
Some(a) => Some(s.doc.eval(&a, Kind::Angle).map_err(|e| bad(cmd, format!("angle: {e}")))?),
None => None,
};
let spec = super::find_command("occurrence.move").ok_or_else(|| EngineError::UnknownCommand("occurrence.move".into()))?;
let mut moved = Vec::new();
for path in &plan.paths {
let Some(&occurrence) = path.last() else { continue };
let mut q = json!({"occurrence": occurrence, "path": path, "space": "world", "translate": translate});
if let Some(axis) = p.get("axis") {
q["axis"] = axis.clone();
}
if let Some(angle) = angle {
q["angle"] = json!(angle);
}
spec.run_nested(s, &q)?;
moved.push(occurrence);
}
let mut out = json!({"placements": moved});
if !plan.bodies.is_empty() {
let mut rest = p.clone();
rest["bodies"] = json!(plan.bodies);
out["bodies"] = move_body_feature(s, &rest)?;
}
Ok(out)
}
fn move_body_feature(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.move";
let bodies = string_list(p, "bodies");
if bodies.is_empty() {
@@ -2127,6 +2178,14 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
(_, Some(x)) => x,
(None, None) => return Err(bad(cmd, "`position` must be [x, y, z], numbers or length expressions (or give `sketch` and `points`)")),
};
// A hole starts on a face: a point off every face (past a body's edge) would drill a hole
// nobody picked, and cutting across an edge that way is slow.
if points.is_none() && !super::face::on_a_face(s, position) {
return Err(bad(
cmd,
format!("`position` [{}, {}, {}] is not on a face of a body; pick a point on the face to drill", position.x, position.y, position.z),
));
}
let direction = match (p.get("direction").and_then(vec3), &points) {
(None, Some((sp, _, d))) if !sp.single => *d,
(d, _) => match d {
@@ -1953,3 +1953,96 @@ fn a_sketch_coil_never_panics_on_hostile_parameters() {
assert!(!matches!(result, Err(crate::EngineError::Internal(..))), "profiles {profiles} panicked: {result:?}");
}
}
fn placed_bounds(s: &Session, name: &str) -> solidcraft_geom::Aabb3 {
s.world_state().body(name).unwrap_or_else(|| panic!("no {name}")).mesh().bounds()
}
fn about(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
/// Frees every grounded placement (a design's first part starts grounded), so a move can take it.
fn unground_all(s: &mut Session) {
let grounded: Vec<u64> = s.doc.occurrences.iter().filter(|o| o.grounded).map(|o| o.id).collect();
for occurrence in grounded {
run(s, "occurrence.ground", json!({"occurrence": occurrence, "grounded": false}));
}
}
/// A part placed twice, the copy turned a quarter about X and set at x = 100: `Blade` at
/// x 0…20, y 0…8, z 0…30 and its copy at x 100…120, y −30…0, z 0…8.
fn twice_placed_blade(s: &mut Session) -> (String, String) {
let made = run(s, "component.create", json!({"name": "Blade"}));
run(s, "solid.box", json!({"length": 20, "width": 8, "height": 30, "body_name": "Blade"}));
run(s, "component.activate", json!({"component": 0}));
let copy = run(s, "occurrence.copy", json!({"occurrence": made["occurrence"]}));
run(s, "occurrence.move", json!({"occurrence": copy["occurrence"], "axis": [1, 0, 0], "angle": "90 deg"}));
run(s, "occurrence.move", json!({"occurrence": copy["occurrence"], "translate": [100, 0, 0], "space": "world"}));
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
assert_eq!(names.len(), 2, "{names:?}");
unground_all(s);
(names[0].clone(), names[1].clone())
}
fn shifted(a: solidcraft_geom::Aabb3, b: solidcraft_geom::Aabb3, by: [f64; 3]) -> bool {
[
(a.min.x, b.min.x, by[0]),
(a.max.x, b.max.x, by[0]),
(a.min.y, b.min.y, by[1]),
(a.max.y, b.max.y, by[1]),
(a.min.z, b.min.z, by[2]),
(a.max.z, b.max.z, by[2]),
]
.iter()
.all(|(was, now, d)| about(was + d, *now))
}
/// #4768: every placement of a part placed twice, moved together, goes exactly the way asked,
/// however each placement is turned: the shared part was moved once per placement listed, each
/// turning the move its own way, so the turned copy went down Z instead of along Y. One undo
/// step puts both back.
#[test]
fn placements_of_one_part_moved_together_each_go_the_way_asked() {
let mut s = Session::default();
let (a, b) = twice_placed_blade(&mut s);
let (a0, b0) = (placed_bounds(&s, &a), placed_bounds(&s, &b));
let undo = s.undo.len();
let r = run(&mut s, "solid.move", json!({"bodies": [a.clone(), b.clone()], "translate": ["240 mm", "-100 mm", "0 mm"]}));
assert_eq!(r["placements"].as_array().map(Vec::len), Some(2), "{r}");
assert!(shifted(a0, placed_bounds(&s, &a), [240.0, -100.0, 0.0]), "{a}: {:?}", placed_bounds(&s, &a));
assert!(shifted(b0, placed_bounds(&s, &b), [240.0, -100.0, 0.0]), "{b}: {:?}", placed_bounds(&s, &b));
assert_eq!(s.undo.len(), undo + 1, "one undo step");
run(&mut s, "edit.undo", json!({}));
assert!(shifted(a0, placed_bounds(&s, &a), [0.0; 3]) && shifted(b0, placed_bounds(&s, &b), [0.0; 3]), "undone");
// A quarter turn about Z moves each placement about the world origin, as Move turns a body.
run(&mut s, "solid.move", json!({"bodies": [a.clone(), b.clone()], "axis": [0, 0, 1], "angle": 90}));
let turned = placed_bounds(&s, &a);
assert!(about(turned.min.x, -8.0) && about(turned.max.x, 0.0) && about(turned.min.y, 0.0) && about(turned.max.y, 20.0), "{turned:?}");
let turned = placed_bounds(&s, &b);
assert!(about(turned.min.x, 0.0) && about(turned.max.x, 30.0) && about(turned.min.y, 100.0) && about(turned.max.y, 120.0), "{turned:?}");
}
/// The same part inserted from a STEP assembly, where a rigid group holds the placements in the
/// imported component: the move goes to the placement of the whole import, once.
#[test]
fn an_imported_assembly_with_a_repeated_part_moves_as_one() {
let dir = std::env::temp_dir().join(format!("solidcraft-4768-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("rep.step").to_string_lossy().to_string();
let mut src = Session::default();
twice_placed_blade(&mut src);
run(&mut src, "file.export", json!({"path": path}));
let mut s = Session::default();
run(&mut s, "file.insert_step", json!({"path": path}));
unground_all(&mut s);
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
assert_eq!(names.len(), 2, "{names:?}");
let before: Vec<_> = names.iter().map(|n| placed_bounds(&s, n)).collect();
let r = run(&mut s, "solid.move", json!({"bodies": names, "translate": [240, -100, 0]}));
assert_eq!(r["placements"].as_array().map(Vec::len), Some(1), "the import's placement, once: {r}");
for (n, b) in names.iter().zip(before) {
assert!(shifted(b, placed_bounds(&s, n), [240.0, -100.0, 0.0]), "{n}: {:?}", placed_bounds(&s, n));
}
let _ = std::fs::remove_dir_all(&dir);
}
+54 -6
View File
@@ -48,12 +48,12 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("file.export", "Export", export)
.icon("export")
.noundo()
.params("path; format?: stl|stla|obj|step|3mf|3mf-tagged|urdf|json (default from extension; 3mf-tagged: 3MF whose triangles carry their B-rep face, at full precision, for meshing and analysis tools; docs/formats/3mf-face-tags.md; urdf: the whole assembly as a robot, with STL meshes written next to it; docs/urdf.md); bodies?: [names] (JSON and URDF export the complete design); urdf only: mesh_prefix? (before each mesh file name, e.g. package://robot/meshes/), effort? (default 1000), velocity? (default 10)"),
.params("path; format?: stl|stla|obj|step|3mf|3mf-tagged|urdf|json (default from extension; 3mf-tagged: 3MF whose triangles carry their B-rep face, at full precision, for meshing and analysis tools; docs/formats/3mf-face-tags.md; urdf: the whole assembly as a robot, with STL meshes written next to it; docs/urdf.md); bodies?: [names] (JSON and URDF export the complete design); urdf only: mesh_prefix? (before each mesh file name, e.g. package://robot/meshes/), effort? (default 1000), velocity? (default 10); quality?: standard|fine|finest (STL, OBJ and 3MF: how closely the mesh follows curved faces, a chord tolerance of 2e-4, 5e-5 or 1e-5 of each body's size; default standard)"),
CommandSpec::new("file.save_mesh", "Save As Mesh", save_mesh)
.at("INSERT", "FILES")
.icon("mesh_export")
.noundo()
.params("path; format?: stl|3mf|3mf-tagged (default from the extension, else stl; 3mf-tagged: docs/formats/3mf-face-tags.md); bodies?: [names] (default: all); ascii?: bool (STL); slicer?: id, name or program path from file.slicers (open the saved file there)"),
.params("path; format?: stl|3mf|3mf-tagged (default from the extension, else stl; 3mf-tagged: docs/formats/3mf-face-tags.md); bodies?: [names] (default: all); ascii?: bool (STL); slicer?: id, name or program path from file.slicers (open the saved file there); quality?: standard|fine|finest (STL, OBJ and 3MF: how closely the mesh follows curved faces, a chord tolerance of 2e-4, 5e-5 or 1e-5 of each body's size; default standard)"),
CommandSpec::new("file.slicers", "Installed Slicers", slicers).icon("export")
.noundo()
.params("→ the slicers found on this machine: [{id, name, icon?}]; pass an id or name as file.save_mesh's `slicer`"),
@@ -535,7 +535,18 @@ fn save_as(s: &mut Session, p: &Value) -> Result<Value> {
)
}
fn export_to(s: &Session, path: &str, format: Format, bodies: &[String]) -> Result<Value> {
/// A mesh export's `quality`: standard (as every export was) unless asked for (#4747).
fn mesh_quality(p: &Value, cmd: &str) -> Result<solidcraft_io::MeshQuality> {
match p.get("quality") {
None | Some(Value::Null) => Ok(solidcraft_io::MeshQuality::default()),
Some(v) => v
.as_str()
.and_then(solidcraft_io::MeshQuality::from_name)
.ok_or_else(|| bad(cmd, format!("`quality` must be standard, fine or finest, not {v}"))),
}
}
fn export_to(s: &Session, path: &str, format: Format, bodies: &[String], quality: solidcraft_io::MeshQuality) -> Result<Value> {
let name = std::path::Path::new(path).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| s.doc.name.clone());
// A design with components exports as a STEP assembly; otherwise bodies as placed.
let bytes = if format == Format::Design {
@@ -548,7 +559,7 @@ fn export_to(s: &Session, path: &str, format: Format, bodies: &[String]) -> Resu
} else {
// Bodies carry the colour they are shown in (appearance, material or imported), and the
// file the design's properties where the format has a place for them.
solidcraft_io::export_with_metadata(&s.doc.painted(&s.world_state()), bodies, format, &name, &s.doc.metadata)?
solidcraft_io::export_with_quality(&s.doc.painted(&s.world_state()), bodies, format, &name, &s.doc.metadata, quality)?
};
solidcraft_io::vfs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(json!({"path": path, "bytes": bytes.len(), "format": format!("{format:?}")}))
@@ -560,7 +571,8 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
if format == Format::Urdf {
return export_urdf(s, path, p);
}
export_to(s, path, format, &string_list(p, "bodies"))
let quality = mesh_quality(p, "file.export")?;
export_to(s, path, format, &string_list(p, "bodies"), quality)
}
/// The assembly as a URDF and its meshes next to it (docs/urdf.md). Every path is checked before
@@ -641,7 +653,8 @@ fn save_mesh(s: &mut Session, p: &Value) -> Result<Value> {
crate::slicers::resolve(spec).ok_or_else(|| bad(cmd, format!("no slicer `{spec}` (see file.slicers, or give the path of a program)")))?,
),
};
let mut out = export_to(s, path, format, &string_list(p, "bodies"))?;
let quality = mesh_quality(p, cmd)?;
let mut out = export_to(s, path, format, &string_list(p, "bodies"), quality)?;
if let (Some(slicer), Some(obj)) = (slicer, out.as_object_mut()) {
// The file is saved either way; a slicer that won't start is reported, not fatal.
match slicer.open(path) {
@@ -715,6 +728,41 @@ mod urdf_tests;
mod tests {
use super::*;
/// #4747: a mesh export's `quality` follows curved faces more closely. A 3 mm hole in a
/// 200 mm plate gets more triangles at fine and more again at finest, in STL and 3MF alike
/// (Save As Mesh too); standard is what the export always gave; anything else is refused.
#[test]
fn a_mesh_export_follows_round_faces_more_closely_at_a_finer_quality() {
let dir = std::env::temp_dir().join(format!("solidcraft-mesh-quality-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 200, "width": 100, "height": 5})).unwrap();
s.execute("solid.cylinder", &json!({"base": [100, 50, -1], "radius": 1.5, "height": 7, "operation": "cut"})).unwrap();
let stl_triangles = |b: &[u8]| u32::from_le_bytes([b[80], b[81], b[82], b[83]]);
let mut counts = Vec::new();
for quality in [None, Some("standard"), Some("fine"), Some("finest")] {
let path = dir.join(format!("plate-{}.stl", quality.unwrap_or("default"))).to_string_lossy().to_string();
let mut p = json!({"path": path});
if let Some(q) = quality {
p["quality"] = json!(q);
}
s.execute("file.export", &p).unwrap();
counts.push(stl_triangles(&std::fs::read(&path).unwrap()));
}
assert_eq!(counts[0], counts[1], "standard is the default: {counts:?}");
assert!(counts[2] > counts[1] && counts[3] > counts[2], "finer, more triangles: {counts:?}");
let finest_3mf = dir.join("plate.3mf").to_string_lossy().to_string();
let standard_3mf = dir.join("plate-standard.3mf").to_string_lossy().to_string();
s.execute("file.save_mesh", &json!({"path": finest_3mf, "quality": "finest"})).unwrap();
s.execute("file.save_mesh", &json!({"path": standard_3mf})).unwrap();
assert!(std::fs::metadata(&finest_3mf).unwrap().len() > std::fs::metadata(&standard_3mf).unwrap().len());
for bad in [json!("ultra"), json!(3), json!(true)] {
let e = s.execute("file.export", &json!({"path": dir.join("bad.stl").to_string_lossy(), "quality": bad})).unwrap_err().to_string();
assert!(e.contains("`quality` must be standard, fine or finest"), "{bad}: {e}");
}
let _ = std::fs::remove_dir_all(&dir);
}
/// #3044: a save run on a scratch copy of the session (on a worker, so the window keeps
/// drawing) and taken up with `adopt_save` leaves the session as saving it directly does:
/// the design named after its first file, its file, and nothing unsaved; undo keeps the name.
+34 -19
View File
@@ -13,7 +13,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("measure")
.key("I")
.noundo()
.params("bodies?: [names] (default all); or items: [1-2 selections] (face/edge: type, body, point; index optional) → length/area/volume; diameter, radius, circumference or arc_length/angle_deg of circles, arcs and cylinders; slot sizes; min (distance_mm, from, to), max, center_distance (centre to centre, or from an axis) and angle"),
.params("bodies?: [names] (default all) → closed per body; volume_mm3, center_of_mass, density_g_cm3 and mass_g only for closed, consistently oriented bodies; area, bbox and counts for all; total.open_bodies_skipped counts bodies excluded from volume/mass totals; or items: [1-2 selections] (face/edge: type, body, point; index optional) → length/area/volume; diameter, radius, circumference or arc_length/angle_deg of circles, arcs and cylinders; slot sizes; min (distance_mm, from, to), max, center_distance (centre to centre, or from an axis) and angle"),
CommandSpec::new("document.inspect", "Inspect Design", inspect).icon("inspect").noundo().params("measure?: bool (include body measurements)"),
CommandSpec::new("sketch.inspect", "Inspect Sketch", sketch_inspect).icon("inspect").noundo().params("sketch?: id|name (default active)"),
CommandSpec::new("model.edges", "List Edges", model_edges).icon("inspect").noundo().params("body: name"),
@@ -53,20 +53,33 @@ pub static COMMANDS: &[CommandSpec] = &[
.params("command: a command id. Returns {id, label, params, input, output} (and param_warnings in the warn parameter-check mode): the command's parameter documentation and, once the command has a typed contract, the JSON Schemas (2020-12) of its parameters and of a successful result (null otherwise). Read-only."),
];
/// Does `body` enclose a volume? A B-rep body does when every shell is closed and consistently
/// oriented (not a surface body), a mesh body when every triangle side is shared by exactly two
/// triangles. Only that is asked: other findings of a full validity check (faces that don't
/// mesh, tolerance gaps of an imported solid) don't take a closed body's volume away, and the
/// check stays cheap for designs with many bodies.
pub(crate) fn encloses_volume(body: &solidcraft_kernel::Body) -> bool {
if body.is_mesh() { body.is_closed_mesh() } else { !body.is_surface() }
}
pub fn measure_json(b: &solidcraft_doc::ModelBody) -> Value {
match b.measure() {
Ok(m) => json!({
"name": b.name,
"volume_mm3": m.volume,
"area_mm2": m.area,
"center_of_mass": m.centroid,
"bbox": {"min": m.bbox.min, "max": m.bbox.max},
"faces": m.merged.faces,
"edges": m.merged.edges,
"vertices": m.merged.vertices,
"face_types": m.merged.face_types,
"kernel": {"faces": m.faces, "edges": m.edges, "vertices": m.vertices, "shells": m.shells},
}),
Ok(m) => {
let closed = encloses_volume(&b.body);
json!({
"name": b.name,
"closed": closed,
"volume_mm3": closed.then_some(m.volume),
"area_mm2": m.area,
"center_of_mass": closed.then_some(m.centroid),
"bbox": {"min": m.bbox.min, "max": m.bbox.max},
"faces": m.merged.faces,
"edges": m.merged.edges,
"vertices": m.merged.vertices,
"face_types": m.merged.face_types,
"kernel": {"faces": m.faces, "edges": m.edges, "vertices": m.vertices, "shells": m.shells},
})
}
Err(e) => json!({"name": b.name, "error": e.to_string()}),
}
}
@@ -112,12 +125,13 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
// and the area (both faces minus the holes, plus the bores).
let open = sh.perforated_area();
if open > 0.0 {
let v = b["volume_mm3"].as_f64().unwrap_or(0.0);
let nv = v - open * sh.t;
if let Some(m) = b["mass_g"].as_f64().filter(|_| v > 0.0) {
b["mass_g"] = json!(m * nv / v);
if let Some(v) = b["volume_mm3"].as_f64() {
let nv = v - open * sh.t;
if let Some(m) = b["mass_g"].as_f64().filter(|_| v > 0.0) {
b["mass_g"] = json!(m * nv / v);
}
b["volume_mm3"] = json!(nv);
}
b["volume_mm3"] = json!(nv);
let bores: f64 = sh
.perforations
.iter()
@@ -127,6 +141,7 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let skipped = bodies.iter().filter(|b| b["closed"] == false).count();
let tv: f64 = bodies.iter().filter_map(|b| b["volume_mm3"].as_f64()).sum();
let ta: f64 = bodies.iter().filter_map(|b| b["area_mm2"].as_f64()).sum();
let tf: u64 = bodies.iter().filter_map(|b| b["faces"].as_u64()).sum();
@@ -134,7 +149,7 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
let tvx: u64 = bodies.iter().filter_map(|b| b["vertices"].as_u64()).sum();
let tm: f64 = bodies.iter().filter_map(|b| b["mass_g"].as_f64()).sum();
Ok(
json!({"body_count": bodies.len(), "bodies": bodies, "total": {"volume_mm3": tv, "area_mm2": ta, "mass_g": tm, "faces": tf, "edges": te, "vertices": tvx}}),
json!({"body_count": bodies.len(), "bodies": bodies, "total": {"open_bodies_skipped": skipped, "volume_mm3": tv, "area_mm2": ta, "mass_g": tm, "faces": tf, "edges": te, "vertices": tvx}}),
)
}
+3 -1
View File
@@ -211,7 +211,9 @@ fn origins_take_a_long_piece_name() {
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 2, "corner": [100, 200, 0]}));
run(&mut s, "solid.hole", json!({"position": [110, 210, 2], "diameter": 6}));
let top = face_name(&mut s, "Tray", Vec3::new(20.0, 20.0, 1.0));
assert!(top.len() > 512, "{}: {top}", top.len());
// Split-face names are bounded at 512 bytes (#5860), so the name is as long as a piece name gets;
// joints take far more than that (`MAX_FACE_NAME`), so none is refused for its length.
assert!(top.len() > 400 && top.len() <= 512, "{}: {top}", top.len());
let wall = face_name(&mut s, "Tray", Vec3::new(53.0, 50.0, 0.5));
let foot = face_name(&mut s, "Bracket", Vec3::new(113.0, 210.0, 1.0));
let under = face_name(&mut s, "Bracket", Vec3::new(105.0, 205.0, 0.0));
+5 -3
View File
@@ -335,12 +335,14 @@ fn describe(s: &Session, sel: &Sel) -> Option<Item> {
Some(Item { geo: Geo::Tris(tris), dir: planar.map(Vec3::from), json, shape, slot })
}
Sel::Body { name } => {
let m = st.body(name)?.mesh();
let body = st.body(name)?;
let closed = crate::cmd::inspect::encloses_volume(&body.body);
let m = body.mesh();
let tris: Vec<[Vec3; 3]> = m.triangles.iter().filter_map(|t| m.tri(t)).collect();
let v = m.measure().volume;
plain(Geo::Tris(tris), None, json!({"type": "body", "name": name, "volume_mm3": v}))
plain(Geo::Tris(tris), None, json!({"type": "body", "name": name, "closed": closed, "volume_mm3": closed.then_some(v)}))
}
Sel::SketchCurve { id } => sketch_curve(sketch_of(s, &st, None, |sk| sk.curve_index(id).is_some())?, id),
Sel::SketchCurve { id, sketch } => sketch_curve(sketch_of(s, &st, *sketch, |sk| sk.curve_index(id).is_some())?, id),
Sel::ExternalSketchCurve { sketch, id } => sketch_curve(sketch_of(s, &st, Some(*sketch), |_| true)?, id),
Sel::SketchPoint { id } | Sel::ExternalSketchPoint { id, .. } => {
let given = match sel {
+31
View File
@@ -620,6 +620,37 @@ fn compact_component_list_counts_and_skips_identity_transforms() {
assert_eq!(block(&run(&mut s, "component.list", json!({"compact": true, "bodies": true})))["bodies"], json!(["Cube"]));
}
/// Each component lists only what it holds, in timeline order: its bodies, features and
/// sketches, its own occurrences and the occurrences inside it, through nested components.
#[test]
fn component_list_gives_each_component_only_its_own_contents() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "name": "Base", "body_name": "Base"}));
let (arm, arm_first) = created(&mut s, "Arm");
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Arm sketch"}));
run(&mut s, "solid.box", json!({"corner": [0, 20, 0], "length": 2, "width": 2, "height": 2, "name": "Lug", "body_name": "Lug"}));
let pin = run(&mut s, "component.create", json!({"name": "Pin", "parent": arm, "parent_path": [arm_first]}));
let (pin, pin_first) = (pin["component"].as_u64().unwrap(), pin["occurrence"].as_u64().unwrap());
run(&mut s, "solid.box", json!({"corner": [0, 40, 0], "length": 1, "width": 1, "height": 1, "name": "Shank", "body_name": "Shank"}));
run(&mut s, "solid.box", json!({"corner": [0, 50, 0], "length": 1, "width": 1, "height": 1, "name": "Head", "body_name": "Head"}));
run(&mut s, "component.activate", json!({"component": "root"}));
let arm_second = run(&mut s, "occurrence.copy", json!({"component": arm, "translate": [30, 0, 0]}))["occurrence"].as_u64().unwrap();
let list = run(&mut s, "component.list", json!({}));
let of = |id: u64| list["components"].as_array().unwrap().iter().find(|c| c["id"] == id).unwrap().clone();
let ids = |v: &Value| v.as_array().unwrap().iter().map(|o| o["id"].as_u64().unwrap()).collect::<Vec<_>>();
let (root, arm_node, pin_node) = (of(0), of(arm), of(pin));
assert_eq!((&root["bodies"], &root["features"], &root["sketches"]), (&json!(["Base"]), &json!(["Base"]), &json!([])), "{root}");
assert_eq!(ids(&root["children"]), vec![arm_first, arm_second], "{root}");
assert_eq!(
(&arm_node["bodies"], &arm_node["features"], &arm_node["sketches"]),
(&json!(["Lug"]), &json!(["Lug"]), &json!(["Arm sketch"])),
"{arm_node}"
);
assert_eq!((ids(&arm_node["occurrences"]), ids(&arm_node["children"])), (vec![arm_first, arm_second], vec![pin_first]), "{arm_node}");
assert_eq!((&pin_node["bodies"], &pin_node["features"]), (&json!(["Shank", "Head"]), &json!(["Shank", "Head"])), "{pin_node}");
assert_eq!((ids(&pin_node["occurrences"]), ids(&pin_node["children"])), (vec![pin_first], vec![]), "{pin_node}");
}
/// A script's 3x4 placement matrix (rotation rows, then the translation column) moves an
/// occurrence exactly as the same turn and move given as an axis, an angle and a translation.
#[test]
+17 -4
View File
@@ -11,7 +11,8 @@ use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("parameters.change", "Change Parameters", change_param).at("SOLID", "MODIFY").icon("params").params(
"name, expression (number or text), unit?: mm|cm|m|in|ft|deg|rad|\"\" (new parameters default to their value's unit), comment?, \
"name, expression (number or text), unit?: mm|cm|m|in|ft|deg|rad|\"\"|text (new parameters default to their value's unit; \
a text parameter's expression is its text, quotes round it optional: sketch texts' words are built from it), comment?, \
favorite?: bool, new_name?: rename (references follow); or delete: name; or changes: {name: expression, …} (existing \
parameters, checked together and rebuilt once, all or nothing) → changed?: {name: value} (with changes), recomputed: features rebuilt, \
restored: features whose earlier result for the same inputs was reused (e.g. a value set back), switched?: [{feature, name, suppressed}] \
@@ -22,7 +23,9 @@ pub static COMMANDS: &[CommandSpec] = &[
"favorites?: bool (only favourites), filter?: all|user|sketch|feature|favorites → parameters (name, expression, unit, value in its unit, \
source, feature/input, comment, favorite, uses, used_by, error) and cycles",
),
CommandSpec::new("parameters.add", "Add User Parameter", add).icon("new").params("name, expression, unit?, comment?, favorite?: bool"),
CommandSpec::new("parameters.add", "Add User Parameter", add)
.icon("new")
.params("name, expression, unit?: as parameters.change (text: a text parameter, its expression the text), comment?, favorite?: bool"),
CommandSpec::new("parameters.rename", "Rename Parameter", rename)
.icon("rename")
.params("name, new_name (expressions, feature inputs and dimensions that use it follow)"),
@@ -53,7 +56,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("params")
.noundo()
.doc_only()
.params("expression, kind?: length|angle|unitless (or unit?: mm|in|deg…) → ok, value (mm/rad), display_value, references, error"),
.params("expression, kind?: length|angle|unitless (or unit?: mm|in|deg…; text: the text a text parameter would hold, as `text`) → ok, value (mm/rad), display_value, references, error"),
CommandSpec::new("parameters.complete", "Complete Expression", complete)
.icon("params")
.noundo()
@@ -79,7 +82,7 @@ fn failing(s: &Session) -> Vec<Value> {
}
pub(super) fn known(s: &Session, name: &str) -> bool {
s.doc.param(name).is_some() || s.doc.features.iter().any(|f| f.param_names.iter().any(|n| n == name))
s.doc.param(name).is_some() || s.doc.text_param(name).is_some() || s.doc.features.iter().any(|f| f.param_names.iter().any(|n| n == name))
}
fn change_param(s: &mut Session, p: &Value) -> Result<Value> {
@@ -132,6 +135,9 @@ fn change_param(s: &mut Session, p: &Value) -> Result<Value> {
}
out["name"] = json!(name);
out["expression"] = json!(e);
if let Some(t) = s.doc.text_param(&name) {
out["text"] = json!(t.text);
}
out["value"] = json!(vals.get(&name).map(|v| v.v));
out["display_value"] = json!(s.doc.param_display_value(&vals, &name));
out["recomputed"] = json!(s.model.last_recomputed);
@@ -217,6 +223,9 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
s.doc_mut().set_favorite(&name, f)?;
}
let (vals, _) = s.doc.param_values();
if let Some(t) = s.doc.text_param(&name) {
return Ok(json!({"name": name, "unit": solidcraft_doc::text_params::TEXT_UNIT, "text": t.text, "value": null}));
}
Ok(
json!({"name": name, "unit": s.doc.param(&name).map(|x| x.unit.clone()), "value": vals.get(&name).map(|v| v.v), "display_value": s.doc.param_display_value(&vals, &name)}),
)
@@ -335,6 +344,10 @@ fn evaluate(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "expr.evaluate";
let e = expr_arg(p, "expression").ok_or_else(|| bad(cmd, "`expression` must be text or a number"))?;
let unit = str_(p, "unit");
// A text parameter's expression is its text.
if unit.is_some_and(solidcraft_doc::text_params::is_text_unit) {
return Ok(json!({"ok": true, "kind": "text", "text": solidcraft_doc::text_params::unquote(&e), "references": []}));
}
if let Some(u) = unit
&& expr::unit_info(u).is_none()
{
+21 -5
View File
@@ -18,8 +18,12 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("sheet.flange", "Flange", flange).at("SHEET METAL", "CREATE").icon("flange").params(
"base: sketch + profiles? (rule?, flip?) | contour: sketch + curves (open chain of lines) + distance (rule?, flip?, reverse?) | \
edge: edges: [[x,y,z] on top/bottom edges, or {point: [x,y,z], angle?, height?} for an edge with its own angle and height] + \
height (to the outer face), angle? (default 90 deg), radius? (default: rule), \
position?: inside|outside|middle, flip?, body?, corner_relief_shape?: notch|round|rectangular|tear and corner_relief? (size across): \
height (along the flange, to its far edge), angle? (default 90 deg), radius? (default: rule), \
height_from?: outer|inner|tangent (where the height is measured from: the outer virtual sharp (default), \
the inner virtual sharp, or past 90 deg the line square to the flange touching the bend's outside (tangent; up to 90 deg the outer sharp)), \
position?: inside|outside|middle|material_outside|tangent (where the bend starts: the outer virtual sharp on the edge (default), \
the bend starting at the edge (bend_outside is the same), half way, the inner virtual sharp on the edge, \
or the bend's outer face tangent to the edge's face (below 90 deg the bend's end on that face)), flip?, body?, corner_relief_shape?: notch|round|rectangular|tear and corner_relief? (size across): \
the relief at the corners these flanges make (default: the rule's); \
then?: [{angle?, height, flip?}] further bends (at most 16), each a flange on the far edge of the one before, flip: true turns that bend the other way (edge only); \
type?: base|edge|contour (default from the inputs); name?, body_name? (the new body). \
@@ -269,10 +273,20 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(r) = &radius {
check_expr(s, r, Kind::Length, cmd, "radius")?;
}
let position = str_(p, "position").unwrap_or("inside").to_ascii_lowercase();
if !matches!(position.as_str(), "inside" | "outside" | "middle") {
return Err(bad(cmd, "`position` must be inside, outside or middle"));
let mut position = str_(p, "position").unwrap_or("inside").to_ascii_lowercase();
if !matches!(position.as_str(), "inside" | "outside" | "bend_outside" | "middle" | "material_outside" | "tangent") {
return Err(bad(cmd, "`position` must be inside, outside, middle, material_outside or tangent"));
}
// The bend starting at the edge is `outside`, whichever name asked for it.
if position == "bend_outside" {
position = "outside".into();
}
// Where the heights are measured from; the outer sharp (the default) is saved as nothing.
let height_from = match p.get("height_from").filter(|v| !v.is_null()) {
Some(v) => v.as_str().and_then(sheet::HeightFrom::parse).ok_or_else(|| bad(cmd, "`height_from` must be outer, inner or tangent"))?,
None => sheet::HeightFrom::Outer,
};
let height_from = (height_from != sheet::HeightFrom::Outer).then(|| height_from.name().to_string());
if s.model.state().sheets.is_empty() {
return Err(no_sheet_body(cmd));
}
@@ -305,6 +319,7 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
body: body.clone().map(BodyName),
corner_relief_shape,
corner_relief,
height_from: height_from.clone(),
},
)?;
// Each further bend stands on the far edge of the flange before it.
@@ -324,6 +339,7 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
body: body.clone().map(BodyName),
corner_relief_shape: None,
corner_relief: None,
height_from: height_from.clone(),
},
)?;
}
+203
View File
@@ -4553,6 +4553,209 @@ fn a_flange_asked_for_0_8_mm_is_0_8_mm_or_refused_never_silently_2_5_mm() {
}
}
/// A 100 x 60 base on XY under a rule 2 mm thick, R 2, K 0.4.
fn r2_base(s: &mut Session) {
run(s, "sheet.manage_rules", json!({"name": "R2", "thickness": "2 mm", "k_factor": 0.4, "bend_radius": "2 mm", "active": true}));
base(s, 100.0, 60.0);
}
/// The top (z) of the sheet and its flat length across the flanged edge.
fn top_and_flat(s: &mut Session) -> (f64, f64) {
let top = bbox(s)["max"][2].as_f64().unwrap_or(f64::NAN);
(top, flat(s)["flat_size_mm"][1].as_f64().unwrap_or(f64::NAN))
}
#[test]
fn an_edge_flange_height_is_measured_from_the_outer_sharp_the_inner_sharp_or_the_bend_tangent() {
// T = 2, R = 2, K = 0.4, height 20 on the edge y = 0, inside position (setback (R + T)·tan(θ/2)).
// Before the leg: outer (R + T)·tan(θ/2), inner R·tan(θ/2), tangent the outer's up to 90° and
// R + T past it.
// The bend's axis is at z = T + R = 4; its far edge is the leg along the flange from the bend's
// end, so the top of the part is 4 − min((R + T)·cos θ, R·cos θ) + leg·sin θ, and the flat is
// 60 − setback + θ·(R + K·T) + leg.
let (t, r, k) = (2.0, 2.0, 0.4);
for deg in [90.0f64, 60.0, 120.0] {
let th = deg.to_radians();
let half = (th / 2.0).tan();
for (from, before) in [("outer", (r + t) * half), ("inner", r * half), ("tangent", if deg <= 90.0 { (r + t) * half } else { r + t })] {
let what = format!("{deg}° from {from}");
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "angle": format!("{deg} deg"), "height_from": from}));
let leg = 20.0 - before;
let top = r + t - ((r + t) * th.cos()).min(r * th.cos()) + leg * th.sin();
let length = 60.0 - (r + t) * half + th * (r + k * t) + leg;
let (got_top, got_flat) = top_and_flat(&mut s);
assert!((got_top - top).abs() < 1e-6, "{what}: top {got_top}, want {top}");
assert!((got_flat - length).abs() < 1e-6, "{what}: flat {got_flat}, want {length}");
}
}
// At 90°: legs 16, 18 and 16, so the far edge is at z = 20, 22 and 20.
let tops: Vec<f64> = ["outer", "inner", "tangent"]
.iter()
.map(|from| {
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "height_from": from}));
top_and_flat(&mut s).0
})
.collect();
for (got, want) in tops.iter().zip([20.0, 22.0, 20.0]) {
assert!((got - want).abs() < 1e-6, "{tops:?}");
}
}
#[test]
fn an_edge_flange_measured_from_the_outer_sharp_saves_and_builds_as_before() {
let build = |extra: Option<&str>| {
let mut s = Session::default();
r2_base(&mut s);
let mut p = json!({"edges": [[50, 0, 2]], "height": 20, "angle": "75 deg"});
if let Some(from) = extra {
p["height_from"] = json!(from);
}
run(&mut s, "sheet.flange", p);
let kind = serde_json::to_value(s.doc.features.last().map(|f| f.kind.clone())).unwrap_or_default();
let (top, length) = top_and_flat(&mut s);
(kind, top, length, volume(&mut s))
};
let (plain, top, length, v) = build(None);
assert!(plain.get("height_from").is_none(), "{plain}");
let (outer, top_o, length_o, v_o) = build(Some("OUTER"));
assert_eq!(plain, outer);
assert_eq!((top, length, v), (top_o, length_o, v_o));
let (inner, ..) = build(Some("inner"));
assert_eq!(inner["height_from"], "inner", "{inner}");
}
#[test]
fn an_edge_flange_too_short_for_its_height_datum_or_with_an_unknown_one_is_refused() {
let mut s = Session::default();
r2_base(&mut s);
let (features, v) = (s.doc.features.len(), volume(&mut s));
let err = |s: &mut Session, p: Value| match s.execute("sheet.flange", &p) {
Ok(v) => panic!("{p} was accepted: {v}"),
Err(e) => e.to_string(),
};
// From the inner sharp at 90° the bend takes R·tan 45° = 2; from the tangent past 90°, R + T = 4.
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 1.5, "height_from": "inner"}));
assert!(e.contains("from the inner sharp must be at least 2.000 mm"), "{e}");
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 3, "angle": "120 deg", "height_from": "tangent"}));
assert!(e.contains("from the bend's tangent must be at least 4.000 mm"), "{e}");
// From the outer sharp the message is the one it always was.
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 3.9, "height_from": "outer"}));
assert!(e.contains("height must be at least 4.000 mm"), "{e}");
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 20, "height_from": "middle"}));
assert!(e.contains("`height_from` must be outer, inner or tangent"), "{e}");
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 20, "height_from": 3}));
assert!(e.contains("`height_from`"), "{e}");
assert_eq!(s.doc.features.len(), features);
assert!(rel(volume(&mut s), v) < 1e-12);
// Up to 90° the tangent is the outer sharp: at 90° the bend takes R + T = 4, at 60° 4·tan 30°.
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 3.9, "height_from": "tangent"}));
assert!(e.contains("from the bend's tangent must be at least 4.000 mm"), "{e}");
let e = err(&mut s, json!({"edges": [[50, 0, 2]], "height": 2.3, "angle": "60 deg", "height_from": "tangent"}));
assert!(e.contains("from the bend's tangent must be at least 2.309 mm"), "{e}");
assert_eq!(s.doc.features.len(), features);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 4, "height_from": "tangent"}));
assert_eq!(s.doc.features.len(), features + 1);
}
#[test]
fn an_edge_flange_measured_from_the_tangent_does_not_jump_as_its_angle_passes_90_degrees() {
// T = 2, R = 2, K = 0.4, height 20 from the tangent, inside position. Before the leg:
// (R + T)·tan(θ/2) up to 90° (the outer sharp), R + T past it; at 90° both are 4, so the leg,
// the top and the flat change smoothly through 90°.
let (t, r, k) = (2.0, 2.0, 0.4);
let build = |deg: f64| {
let th = deg.to_radians();
let half = (th / 2.0).tan();
let before = if deg <= 90.0 { (r + t) * half } else { r + t };
let leg = 20.0 - before;
let top = r + t - ((r + t) * th.cos()).min(r * th.cos()) + leg * th.sin();
let length = 60.0 - (r + t) * half + th * (r + k * t) + leg;
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "angle": format!("{deg} deg"), "height_from": "tangent"}));
let (got_top, got_flat) = top_and_flat(&mut s);
assert!((got_top - top).abs() < 1e-6, "{deg}°: top {got_top}, want {top}");
assert!((got_flat - length).abs() < 1e-6, "{deg}°: flat {got_flat}, want {length}");
(got_top, got_flat)
};
// 60°: leg 20 − 4·tan 30° = 17.691, as from the outer sharp; the inner face's end is highest
// at the bend, 4 − R·cos 60° = 3.
let (top60, _) = build(60.0);
let want = 4.0 - 2.0 * 0.5 + (20.0 - 4.0 * (30f64.to_radians()).tan()) * (60f64.to_radians()).sin();
assert!((top60 - want).abs() < 1e-6, "60°: top {top60}, want {want}");
// 90°: leg 16, top 20.
let (top90, flat90) = build(90.0);
assert!((top90 - 20.0).abs() < 1e-6, "90°: top {top90}");
// A thousandth of a degree either side moves the top and the flat by hundredths at most.
for deg in [90.0 - 1e-3, 90.0 + 1e-3] {
let (top, flat) = build(deg);
assert!((top - top90).abs() < 1e-2 && (flat - flat90).abs() < 1e-2, "{deg}°: top {top} flat {flat}, at 90° {top90} {flat90}");
}
}
#[test]
fn edge_flange_material_outside_and_tangent_positions_start_the_bend_where_they_say() {
// T = 2, R = 2, K = 0.4, height 20 from the outer sharp on the edge y = 0. Setback (where the
// bend starts, inside the edge): material_outside R·tan(θ/2), tangent R + T, outside 0, and
// bend_outside the same as outside. The flange's outermost point (least y) is the setback less
// what reaches furthest out: R + T past 90° (the bend's outside a quarter turn in), at 90°
// the outer face, R + T. The flat is 60 − setback + θ·(R + K·T) + 20 − (R + T)·tan(θ/2).
let (t, r, k) = (2.0, 2.0, 0.4);
for deg in [90.0f64, 120.0] {
let th = deg.to_radians();
let half = (th / 2.0).tan();
for (position, setback) in [("material_outside", r * half), ("tangent", r + t), ("outside", 0.0), ("bend_outside", 0.0)] {
let what = format!("{deg}° {position}");
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "angle": format!("{deg} deg"), "position": position}));
let least_y = bbox(&mut s)["min"][1].as_f64().unwrap_or(f64::NAN);
assert!((least_y - (setback - (r + t))).abs() < 1e-6, "{what}: least y {least_y}");
let length = flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(f64::NAN);
let want = 60.0 - setback + th * (r + k * t) + 20.0 - (r + t) * half;
assert!((length - want).abs() < 1e-6, "{what}: flat {length}, want {want}");
let saved = serde_json::to_value(s.doc.features.last().map(|f| f.kind.clone())).unwrap_or_default();
assert_eq!(saved["position"], if position == "bend_outside" { "outside" } else { position }, "{what}");
}
}
// Below 90° the tangent position puts the bend's end on the edge's face: at 60° the setback
// is (R + T)·sin 60° = 3.464, the leg 20 − (R + T)·tan 30° = 17.691 leans on out past the
// face, so the least y is −leg·cos 60° = −8.845. Through 90° the setback does not jump.
let th = 60f64.to_radians();
let (setback, leg) = ((r + t) * th.sin(), 20.0 - (r + t) * (th / 2.0).tan());
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "angle": "60 deg", "position": "tangent"}));
let least_y = bbox(&mut s)["min"][1].as_f64().unwrap_or(f64::NAN);
assert!((least_y + leg * th.cos()).abs() < 1e-6, "60° tangent: least y {least_y}, want {}", -leg * th.cos());
let length = flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(f64::NAN);
let want = 60.0 - setback + th * (r + k * t) + leg;
assert!((length - want).abs() < 1e-6, "60° tangent: flat {length}, want {want}");
let flat_at = |deg: f64| {
let mut s = Session::default();
r2_base(&mut s);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20, "angle": format!("{deg} deg"), "position": "tangent"}));
flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(f64::NAN)
};
let (below, at, above) = (flat_at(90.0 - 1e-3), flat_at(90.0), flat_at(90.0 + 1e-3));
assert!((below - at).abs() < 1e-2 && (above - at).abs() < 1e-2, "flats {below} {at} {above}");
// The default is still inside, and an unknown position is refused without a feature.
let mut s = Session::default();
r2_base(&mut s);
let features = s.doc.features.len();
let e = s.execute("sheet.flange", &json!({"edges": [[50, 0, 2]], "height": 20, "position": "flush"})).unwrap_err().to_string();
assert!(e.contains("`position` must be inside, outside, middle, material_outside or tangent"), "{e}");
assert_eq!(s.doc.features.len(), features);
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2]], "height": 20}));
let saved = serde_json::to_value(s.doc.features.last().map(|f| f.kind.clone())).unwrap_or_default();
assert_eq!(saved["position"], "inside");
assert!((bbox(&mut s)["min"][1].as_f64().unwrap_or(f64::NAN)).abs() < 1e-6);
}
/// A new fold still refuses a `fixed` point in a cut-out or past the outline, leaving the design
/// untouched (#263); a stored one that drifts off the sheet keeps its side with a warning (#370).
#[test]
+3 -1
View File
@@ -120,7 +120,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.params(concat!("p0, p1: overall ends, width", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y] (each a number or an expression over the design's parameters: the point is then fixed where it comes to and follows them) or a snap: \"mid:<line>\" (held at the line's midpoint), \"on:<curve>:x,y\" (on the curve near x,y), {vertex: [x,y,z]} or {on_edge: [x,y,z]} of the model; id?", connect_doc!(), created_doc!())),
CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params(
"entities: [refs] (a line midpoint is \"mid:line_id\"), type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current; a bare number is in the design's units, e.g. 2 means 2 in in an inch design; give a unit such as \"50 mm\" to override), or `name = expression` (e.g. \"width = 40\"; also `#name = expression`), which adds the user parameter `name` with that expression and dimensions with `name` (refused when the name is taken or reserved; the reply's `defined` names it); text_at?: [x, y]. Two points or a line off the axes: with auto, text_at across from them is the distance between them, above or below them horizontal, beside them vertical. Two midpoints: placing text above or below their bounding box measures horizontally, beside it vertically. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. A line and a circle or arc: the distance from the line to its edge (the gap, or how far the edge reaches past a line that cuts it; 0 is tangent); dimension the centre point for the distance to the centre. A point and a circle or arc: the distance from the point to its edge (or how far within it a point inside lies). Two circles or arcs: the gap between their edges (or how far they overlap when they cross, or the narrowest gap between them when one lies inside the other); dimension their centre points for the distance between centres. A dimension the sketch already determines is refused (naming what determines it); if_redundant?: \"driven\" places it as a driven (reference) dimension instead. scale_sketch?: bool (the dimension tool sends true): when this is the sketch's first length dimension and nothing in the sketch fixes a size (no other driving length dimension, nothing fixed or projected), the whole sketch scales about its origin to the value instead of distorting. Returns measured in unit (mm or deg), and scaled: the factor when the sketch was scaled",
"entities: [refs] (a line midpoint is \"mid:line_id\"), type?: auto|distance|horizontal|distance_x|vertical|distance_y|length|radius|diameter|angle, value?: number or expression (default: current; a bare number is in the design's units, e.g. 2 means 2 in in an inch design; give a unit such as \"50 mm\" to override), or `name = expression` (e.g. \"width = 40\"; also `#name = expression`), which adds the user parameter `name` with that expression and dimensions with `name` (refused when the name is taken or reserved; the reply's `defined` names it); text_at?: [x, y]. Two points or a line off the axes: with auto, text_at across from them is the distance between them, above or below them horizontal, beside them vertical. Two midpoints: placing text above or below their bounding box measures horizontally, beside it vertically. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. A line and a circle or arc: the distance from the line to its edge (the gap, or how far the edge reaches past a line that cuts it; 0 is tangent); dimension the centre point for the distance to the centre. A point and a circle or arc: the distance from the point to its edge (or how far within it a point inside lies). Two circles or arcs: the gap between their edges (or how far they overlap when they cross, or the narrowest gap between them when one lies inside the other); dimension their centre points for the distance between centres. A dimension the sketch already determines is refused (naming what determines it); if_redundant?: \"driven\" places it as a driven (reference) dimension instead. scale_sketch?: bool (the dimension tool sends true): when this is the sketch's first length dimension and nothing in the sketch fixes a size (no other driving length dimension, nothing fixed or projected), the whole sketch scales about its origin to the value instead of distorting. Returns measured in unit (mm or deg), and scaled: the factor when the sketch was scaled",
),
CommandSpec::new("sketch.constraint.horizontal_vertical", "Horizontal/Vertical", c_horizontal_vertical)
.at("SKETCH", "CONSTRAINTS")
@@ -2319,6 +2319,8 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
let ty = match (ty.as_str(), str_(p, "orientation").map(str::to_ascii_lowercase).as_deref()) {
("distance" | "auto", Some("horizontal")) => "horizontal".to_string(),
("distance" | "auto", Some("vertical")) => "vertical".to_string(),
("distance_x", _) => "horizontal".to_string(),
("distance_y", _) => "vertical".to_string(),
("offset", _) => "distance".to_string(),
_ => ty,
};
+6 -3
View File
@@ -78,7 +78,7 @@ fn chosen(s: &Session, p: &Value) -> Vec<String> {
s.selection
.iter()
.filter_map(|sel| match sel {
Sel::SketchCurve { id } | Sel::SketchPoint { id } => Some(id.clone()),
Sel::SketchCurve { id, .. } | Sel::SketchPoint { id } => Some(id.clone()),
_ => None,
})
.collect()
@@ -321,8 +321,11 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
}
// The pasted geometry is selected, ready to move.
if s.active_sketch == Some(target) {
s.selection =
curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).chain(lone.iter().map(|id| Sel::SketchPoint { id: id.clone() })).collect();
s.selection = curves
.iter()
.map(|id| Sel::SketchCurve { id: id.clone(), sketch: None })
.chain(lone.iter().map(|id| Sel::SketchPoint { id: id.clone() }))
.collect();
}
Ok(json!({"curves": curves, "points": lone, "constraints": constraints, "translate": [t.x, t.y], "sketch": info}))
}

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