Compare commits

...

114 Commits

Author SHA1 Message Date
bherbruck 65d288227e Release 0.21.0 (rc.2's code, version only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:51:08 -04:00
bherbruck fedb3bcdb1 Release 0.21.0-rc.2 2026-10-23 17:14:38 -04:00
bherbruck a51dfc9e1f Release 0.21.0-rc.2 (#5691)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:14:35 -04:00
bherbruck 9aaa8016e5 gpu tests: give the two test frames the new hover and selection tints (#5698)
#5109 added Frame::hover_tint and selection_tint; two test frames that landed alongside it didn't set them, so solidcraft-gpu's tests stopped compiling on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:04:32 -04:00
bherbruck 632ec4157c Sketch on a picked origin plane uses the plane shown after standing an import up (#5659)
* Sketch on a picked origin plane uses the plane shown, also in a moved component

After a STEP file opens its component stays the active one, and standing it up
turns that component a quarter about X. Starting a sketch on XY picked in the
viewport then sent `{"plane": "XY"}`, which names the active component's own
XY: upright after the stand-up. The sketch landed on the world XZ, Look At
turned to face Y and the extrude went along Y.

The viewport and browser only show the design's origin planes, so a picked
origin plane now goes to sketch.create written out as that world plane while
the active component is moved; sketch.create maps it into the component's
frame once. The flip toward the viewer uses the same plane.

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

* Stand-up axes tests: stand the file up through the offer when one is waiting

#5655 offers to stand up SolidWorks files instead of turning them silently.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:11:39 -04:00
bherbruck c3ed8b1c07 Offer to stand up SolidWorks files instead of turning them silently; add Turn Up Axis to the component menu (#5655)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:05:46 -04:00
Jackson Oursland 8301d1c48e Viewport: whole-body highlights reuse the model's own triangles (#4554) (#5109)
Hovering or selecting whole bodies -- a browser row, a History group, a body
under the pointer in body-pick mode -- built a CPU copy of every body's
triangles into the highlight scene. On a 40-body 4.3 MB STEP assembly that was
147 bodies, 70 MB of triangle copies and a 519 ms frame.

The model already has the same geometry on the GPU. The highlight now names it
and the renderer draws it again in the highlight colour:

- bodies drawn from a display plan: the model's evaluated plan chunks, with a
  hover/selection tint uniform (the chunks stay resident in the model scene);
- bodies drawn as placements of shared triangles: the same placements of the
  model's shared triangles, coloured per instance;
- bodies the model builds on the CPU: their vertex range in the model scene's
  block.

See-through and faded bodies keep the CPU feedback, which colours them; faces
and edges highlighted on their own are unchanged. The highlight looks the same
(verified side by side).

On the assembly the highlight build drops from 519 ms to 9 ms and no longer
copies any triangles; selecting the whole assembly takes 9-20 ms.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 16:05:41 -04:00
github-actions[bot] 2a02eb71ac Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 20:04:02 +00:00
bherbruck 097b8b7550 Release 0.21.0-rc.1
The tree is main at f120285c9 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:48:05 -04:00
bherbruck f120285c96 Fix design format 33 for 0.21.0-rc.1 (#5610)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:47:48 -04:00
bherbruck af7ca3a8a2 Release 0.21.0-rc.1
The tree is main at b31fddb98 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:37:43 -04:00
bherbruck b31fddb98e OpenGL: the viewport scene renders 1x again, like the window (#5605)
#5187 gave the scene its own offscreen target with the app's sample count,
which skipped the OpenGL clamp to 1x from 6dd202d. The scene's count now
goes through effective_msaa_samples as well.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:37:27 -04:00
bherbruck 27ddb725c7 Release 0.21.0-rc.1
The tree is main at 2bf3d4e11 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 12:10:02 -04:00
bherbruck 2bf3d4e11f Release 0.21.0-rc.1 (#5566)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:09:47 -04:00
github-actions[bot] 9852a2e758 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 16:05:55 +00:00
raykholo 17bac141c8 An intersect that would leave nothing is refused instead of deleting the body (#5295) (#5303)
* claim: #5295

* An empty intersect is refused instead of deleting the body (#5295)

An intersect whose boolean came out empty removed the target body and
reported success. The shared evaluator now refuses it when nothing of a
named target would remain, when nothing of any target would remain, or
when the tools meet a target and the result is still empty. An unnamed
target the tools miss altogether is still removed, as an intersect with
every body does (#5160). Sheet metal flat machining follows the same rule.

* Sheet metal: an intersect with already removed material is refused (#5295)
2026-10-23 13:50:47 +00:00
gsrunion 4b42ef50c7 Mesh leftovers from the ribbon redesign (#133) (#4471)
* claim: #133

* mesh: Save As Mesh declares the panel that shows it (#133)

The ribbon redesign (#2176) removed the MESH tab, but three things still
pointed at it:

- `file.save_mesh` declared `.at("MESH", "MODIFY")`. The command inventory
  prints the placement a spec declares, so `docs/features.md` advertised a
  `MESH > MODIFY` location no ribbon has. Its real home is already
  INSERT / FILES (`SECTIONS` lists it), so the declaration now agrees with
  the ribbon; `destination()` resolved it there before and after, so nothing
  moves on screen.
- The user guide sent people to "the Mesh tab under Modify" for Save as mesh.
- A test now refuses any command that declares a tab the ribbon removed, and
  pins where Save As Mesh lives.

`PLACEMENTS` keeps its MESH entries: those migrate toolbars users saved under
the old panels, which is a different question from what a spec declares.

---------

Co-authored-by: Geraldrunion <geraldrunion@users.noreply.github.com>
2026-10-23 13:45:35 +00:00
Peppe Bergqvist fdde3f9da9 Add macOS native menu bar (#2361) (#4531)
* claim: #2361

Co-Authored-By: GitHub Copilot <noreply@github.com>

* feat: add native macOS menu bar (#2361)

Reuse document actions and unsaved quit handling in native application, File, Edit, Window and Help menus. Replace duplicate header controls only when native installation succeeds, preserving other platforms and the fallback UI.

Co-Authored-By: GitHub Copilot <noreply@github.com>

---------

Co-authored-by: GitHub Copilot <noreply@github.com>
2026-10-23 13:45:31 +00:00
Colin Faulkingham 74ea81519b Keep Parameters responsive in large feature histories (#2733) (#4549)
* claim: #2733

* Keep Parameters responsive with many feature groups (#2733)
2026-10-23 13:45:26 +00:00
SS-EN 769dfff1a5 Section Analysis: a Hatch option (#4563) (#4565)
* claim: #4563

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

* Section Analysis: a Hatch option, off for a plain cut in the body's colour (#4563)

The Section Analysis dialog gets a Hatch check box beside Flip, and
inspect.section a hatch?: bool (default true), so scripts, the CLI and the
control channel reach it too. Hatched, a cut is its body's colour tinted
toward the cap colour with 45 degree stripes, as before; off, it is the
body's own solid colour. The viewport (cap alpha in the uniform), the CPU
rasterizer and ui.render follow it, and the section keeps it when hidden
and shown again or edited.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:23 +00:00
SS-EN 3ef68c708f Expression suggestions stay closed while another control is dragged (#4589) (#4590)
* claim: #4589

* Expression suggestions stay closed while another control is dragged (#4589)

A drag elsewhere leaves a text box its focus (egui gives it up on a click,
not a drag), so dragging the Drive Joints slider rewrote the focused Angle
box under its old selection. A letter before the caret ("5.0 d|eg") made a
new word, a fresh undismissed popup, and the list opened over the slider.

Dismissed suggestions now stay closed until the box is typed in, a drag of
another widget dismisses them, and a hidden list forgets its row rects.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:17 +00:00
Reefwalker 3ed1829cc7 fix: retain circular sketch pattern Quantity expressions (#4435) (#4607) 2026-10-23 13:45:15 +00:00
raykholo b982ccd8c4 Mesh Convert works without activating the mesh's component first (#4609) (#4613)
* claim: #4609

* Mesh Convert works without activating the component first (#4609)

Inserting a mesh puts it in a new component while the parent stays
active, so Convert Mesh always refused the first time. Convert now
resolves the mesh body's owning component and adds the conversion
there when the active component is that component or an ancestor,
leaving the active component unchanged. From another branch it still
refuses, and the error names the component to activate.
2026-10-23 13:45:13 +00:00
dudedagger42 2586b09f5a Control: a whole-design measure reads a copy off the window thread (#4604) (#4614)
* Control: a whole-design measure reads a copy off the window thread

`inspect.measure` and `document.inspect` (which measures every body by
default) ran on the window thread. On a large assembly that took about a
minute, the window answered nothing meanwhile, and Windows closed it as
not responding (#4604). They change nothing, so they now run on a copy of
the session on a worker and are answered from there, like exports, but
whatever the "agent commands off the window thread" preference says: an
answer from the design as it was when asked has no order to keep. The
window keeps answering `ui.*` and drawing meanwhile.

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

* Control: clearance and interference checks read a copy off the window thread too

inspect.clearance and inspect.interference only read the design, and on a
large assembly measure every pair of bodies: on a 153-body design a clearance
check computed for over five minutes on the window thread, the window showing
"not responding". They join inspect.measure and document.inspect in running on
a copy, answered from there.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:10 +00:00
DuBearte a5f9ff45dd Sketch Symmetry: a dimensioned shape slides onto the axis (#4030) (#4626)
* claim: #4030

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

* Sketch Symmetry: a dimensioned shape slides onto the axis (#4030)

With a rectangle's width dimensioned, Symmetry of its two sides about an
axis was refused: the second side can't move on its own, the axis never
moves, and the ordinary solve that slides the whole rectangle only ran
when every first-picked entity could move. One movable first entity is
now enough.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:08 +00:00
raykholo 61360c3b0d Convert to Sheet Metal: a bend picked on its tangent edge connects, on rotated parts too (#4328) (#4630)
* claim: #4328

* Fix sheet conversion picks on tangent bend edges (#4328)

Resolve cylindrical bends through the nearest edge's incident faces when a
pick on a tangent boundary resolves to its planar panel. This keeps valid
remote bends connected, including picks on the other skin of a shell.

Keep conversion geometry and picks in the body's local frame, and format
conversion diagnostics through its world placement. Cover rotated bend
picks and disconnected-pick coordinates with synthetic regressions.
2026-10-23 13:45:06 +00:00
Reefwalker eec6da060f Measure: report obtuse angles at a shared corner (#4625) (#4634)
* claim: #4625

* fix: measure obtuse corners independently of edge direction (#4625)
2026-10-23 13:45:04 +00:00
DuBearte f422bb1263 Intersection edges divide at 1e-6 when asked for less, instead of panicking (#4623) (#4650)
A join failed with the kernel's internal error "tolerance must be no less
than 1e-6" at vendor/truck-geometry/src/decorators/intersection_curve.rs:172:
IntersectionCurve::parameter_division asserted truck's minimum tolerance,
and the guard turned the panic into a failed operation. A tolerance below
truck's TOLERANCE, or one that is not a number, now divides at TOLERANCE:
the curve's points are only found to within it anyway.

The test divides the intersection edges of a hole cut through a B-spline
face at 5e-7, 1e-12, 0, -1 and NaN: each gives the division at 1e-6. Without
the fix it panics with the reported message at the reported line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:56 +00:00
Martin Pålsson 96ce31153c Pattern and mirror: a rib as the source (#4593) (#4667)
* claim: #4593

* Pattern and mirror: a rib as the source (#4593)

A pattern or mirror of features refused a rib as a source ("Rib1
cannot be patterned or mirrored yet"). A rib's walls run until they
meet the body, so each copy is built again where it is: the rib's
sketch is moved by the copy's transform and the walls are run against
the bodies there, as a hole is drilled again at each copy. The copies
join the body, as the rib does. The rib's own tool building moves into
rib_tools, which the rib feature calls unmoved.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:52 +00:00
Steve Jason a9c3c8bb8a Accept a leading = in expressions, so a fillet radius can be =d3 (#4681) (#4686)
* Sketch fillet radius: accept a leading = in an expression (#4681)

* A second = after a name is still not a definition
2026-10-23 13:44:48 +00:00
dudedagger42 a147a7c083 Sketch Point: a point put on a midpoint or a curve snap is held there (#4691) (#4745)
* Sketch Point: a point put on a midpoint or a curve snap is held there (#4691)

The Point tool sent only a snapped point's coordinates and sketch.point
took only [x, y], so a point clicked on a line's midpoint (or on a curve,
or a model vertex) was free to move off it. sketch.point now takes the
snap forms the other draw commands take ("mid:<line>", "on:<curve>:x,y",
{vertex}, {on_edge}); the snap makes the point with its constraint. The
tool passes those snaps as themselves; a snap to an existing point still
goes as coordinates, where `connect` joins them.

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

* Sketch Point: refuse a snap it can't read instead of answering as if it made a point

sketch_project::snap_points leaves a snap it can't parse ("on:c1:abc",
"on:c1", "on:c1:1,2,3") as it was given, and sketch.point then read
that string back as the point it had made: no error, no point, and an id
a script would go on to dimension. It now takes the snap only when it
names a point of the sketch, and otherwise refuses, listing the snap
forms (found by the test agent on #4745).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:42 +00:00
dudedagger42 89a11d75c0 Imports through the app keep their matrix, name and options (#4676) (#4746)
The app's background loader (file_load::control) kept only `up` and
`names` of a control-channel open or import, so file.insert_step's
`matrix` and `name` were dropped and the part landed at the origin, and
file.insert_mesh lost all its options. It keeps every parameter but the
path, as given; each command checks them as it does when run directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:40 +00:00
dudedagger42 84cad43154 Timeline marker: refuse a negative position, and name the command asked for (#4712) (#4749)
timeline.rollback / timeline.roll_to read a negative position as 0, so
an agent passing -1 for "one step back" rolled the whole timeline back
and every body disappeared, with no error. A negative position is now
refused. The two commands shared one function that refused in
roll_to's name; each now refuses in its own.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:38 +00:00
rlartot 5c280b9a94 fr: Répétition instead of Réseau for patterns (#4751)
Nine messages: the pattern tools, their headings and two sample
descriptions. "Réseau" can be mistaken for a network.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 13:44:36 +00:00
DuBearte 1a953736ff Combine takes bodies clicked in the Browser (#4452) (#4766)
* claim: #4452

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

* Combine takes bodies clicked in the Browser (#4452)

A body row in the Browser only replaced the selection, so with Combine
open its target and tools stayed empty and overlapping bodies could only
be picked in the view. When the open dialog's active input takes bodies,
a body row now goes there, as plane and axis rows already do.

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

* Combine Browser picks: test both layouts and control-channel clicks (#4452)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:32 +00:00
DuBearte 4106332738 Shortcuts: a sketch tool and a model command can share a key (#4163) (#4781)
The Keyboard Shortcuts page refused a key another command used, even a
sketch tool and a model command that never run in the same mode, so
Shift+S could not open Create Sketch in the model and place a point in a
sketch. Edited by hand, the press ran Create Sketch inside the sketch.

A sketch tool (SKETCH tab) and a model command (the other tabs) may now
share a key. A press runs the enabled command of the mode the view is in
first: the tool in a sketch, the model command outside. Two commands of
one mode still clash, and commands on no tab (undo, views, files) clash
with everything.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:29 +00:00
CamWight 9559ad398a Physical material: one rule for a body's material and its density (#4783) (#4785)
* claim: #4783

* Physical material: one rule for a body's material and its density (#4783)

inspect.measure, component.properties, occurrence.properties and the
Properties window took a body's material name from the raw assignment,
but its density from Document::density, which falls back to the default
material when the assigned one is unknown or has an unusable density. So
a damaged or hand-edited design read one material and weighed another: a
design material "Broken" at -3 g/cm³ read "Broken" at 1.29 g/cm³.

One rule, in one place:
- DEFAULT_MATERIAL (new, beside DEFAULT_DENSITY in the material library)
  is the default material's name. The library's first row uses it.
- Document::body_material (new, beside Document::density) is the material
  a body's mass is computed with, as the library or the design spells it:
  the assigned one when it is known and has a usable density, else
  DEFAULT_MATERIAL. Document::density is that material's density, with
  the same value as before for every input.

inspect.measure and the physical properties name body_material.
inspect.measure now names every body's material, measured or not, since
naming it takes no measuring; the density stays with the mass, on the
bodies measured. So the Properties window shows the engine's name without
a fallback of its own. The places that keep the assignment as stored
(configuration cells, material.assign, material.edit, material.delete
and the dialogs) only use the constant, so a configuration still keeps a
damaged assignment as it is.

The visible change: a body whose material is unknown, or has an unusable
density, reads "Default", the material it weighs, and a miscased name
reads as the library or the design spells it. A body inspect.measure
can't measure now has its material too. Masses and densities don't
change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:27 +00:00
DuBearte bddb52c96a Move: the value box follows the dragged axis, and Tab steps through X, Y, Z (#4776) (#4788)
The Move dialog's floating value box always showed and edited the X
distance, whichever arrow was dragged, and Tab left the box for another
widget. Now:
- dragging an arrow shows that axis's distance, turning a ring (Free Move)
  that axis's angle; a plane square or the ball leaves the box as it is;
- Tab in the box goes to the next value (X, Y, Z, then in Free Move the
  X, Y and Z angles) and Shift+Tab back, the keyboard staying in the box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:25 +00:00
raykholo a0fd39a7be Flange on a plain solid says how to start sheet metal (#2987) (#4789)
* claim: #2987

* Flange on a plain solid says how to start sheet metal (#2987)

Flange, Flat Pattern, and Export DXF on a design with no sheet metal body
now say how to make one: Flange > Base, or Convert to Sheet Metal on a
plate's large flat face. An edge flange checks for a sheet metal body
before it adds a feature. The message is in the catalogs, and engine
messages that match a catalog entry are shown in the interface language.

* Dialogs show a refusal without the engine's parameter prefix (#2987)

The Flange dialog showed "invalid parameters for `sheet.flange`: " in front
of the new guidance. The dialog already names its command, so the red
refusal text leaves that prefix out and follows the interface language
when the message is in the catalogs. Create Issue still gets the full text.
2026-10-23 13:44:24 +00:00
raykholo be87303683 Web: a trackpad swipe pans instead of zooming (#4116) (#4834)
* claim: #4116

* Web: a trackpad swipe pans instead of zooming (#4116)

Browsers deliver trackpad swipes as wheel events without scroll phases, so
the shared navigation treated every vertical swipe as a mouse-wheel zoom.
The web raw-input hook now brackets point-mode swipes (small or horizontal
deltas) with gesture start/end phases, ending after 100 ms idle or before
line/page wheel or pinch input. Wheel notches and pinch still zoom, and
camera input still goes through navigation::handle.
2026-10-23 13:44:20 +00:00
raykholo bd0cc4bb51 Units of new designs apply to the empty design already open (#221) (#4835)
* claim: #221

* Units of new designs apply to the blank design already open (#221)

Changing Preferences > Units of new designs only took effect at the next
File > New, so the empty design open at launch stayed in millimetres. When
the preference changes (the dialog, Cancel, Restore Defaults, or
preferences.set) and the open design is blank and unsaved, it takes the new
units at once, without an undo step or unsaved changes. A design with
content or a file keeps its units.
2026-10-23 13:44:18 +00:00
raykholo 4c8ae5d078 Hem: the direction arrow points the way the hem builds (#4794) (#4836)
* claim: #4794

* Hem: the direction arrow points the way the hem builds (#4794)

The Hem dialog shared the edge flange's arrow, which points out of the
sheet's larger neighbouring face. A hem folds back the other way, so the
arrow pointed away from the material it adds. Flip the hem's arrow so it
matches the geometry; edge flanges keep theirs.

A regression test compares the arrow with the side the finished hem adds
material on, for both Flip states, top and bottom edge picks, and both
dialog layouts.
2026-10-23 13:44:17 +00:00
Mateusz Sych e33c371e99 Model cache: thin the checkpoints of a design too big for its file instead of leaving the cache out (#3220) (#4882)
* claim: #3220

* Model cache: keep fewer states of a design too big for its file instead of none (#3220)

A model cache over the container's budget keeps the final state, then the
slowest features' states while the bodies they add fit (a body several
states hold counts once), and is left out only when not even the final
state fits. Opening evaluates just the features whose state the file left
out, each from the state before it, so the model is whole afterwards.

* Model cache: keep each encoded body apart, copy only what a cache stores

Encoding a model too big for its file no longer copies every distinct body
into one buffer before thinning: the payload is copied from the encodings
once its size fits, so saving holds no more than before.

* Model cache tests: canonical runs in the timeline fixture; rustfmt

* Model cache: count the records a thinned cache keeps; always try the final state

Thinning counted only the bodies, so a model of many copies of one part
(records, not bodies, filling the room) shrank its budget to nothing and
saved no cache although its final state fitted. The choice now counts the
records of what it keeps, always keeps the final state, and says what it
counted, so each retry keeps less and the final state alone is always tried.

* docs: a thinned model cache counts its records and always tries the final state

* Model cache test: the slowest feature's state holds bodies 16 to 23

* Model cache: thinned opens with fingerprints that read the state before a feature

Since #3397 a feature's fingerprint reads where the bodies before it lie.
After a state the file left out, adopt takes the saved fingerprint and the
evaluation that works the state out checks it. A result evaluated ahead
is never taken as one the file left out. The thinned round trips include
the placed components' design, whose cut reads those placements.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-23 13:44:11 +00:00
raykholo e104e241a3 Rename a body: keep the names of siblings a later feature removed (#4880) (#4904)
* claim: #4880

* Rename a body: keep the names of siblings a later feature removed (#4880)

Renaming a body sets the stored names of all bodies its feature made, so
the siblings are not renumbered. Those names were read from the final
model, where a body that a later feature removed no longer exists: its
stored name was lost, it came back as `Ext (3)`, and the later Remove
failed with "no body `Body3`". Read the siblings from the feature's own
result instead, as the later features already are.

* Test: renaming one half of a split leaves the other

Renaming one half keeps the other half's name, also once a later feature
joins that other half away, after both an incremental and a full
evaluation.
2026-10-23 13:44:09 +00:00
raykholo 3bca787213 MCP tools: unknown command ids suggest the closest ids (#4846) (#4915)
* claim: #4846

* MCP tools: unknown command ids suggest the closest ids (#4846)

An unknown command id in `execute` or `batch` now lists up to three closest
ids, and an id with parameters packed into it says to pass them in `params`.
2026-10-23 13:44:07 +00:00
raykholo 21257c39c2 Keyboard Shortcuts page: draw only the visible rows (#4804) (#4919)
* claim: #4804

* Keyboard Shortcuts page: draw only the visible rows (#4804)

Off-screen rows keep their height but skip their cells, the list is
filtered once before measuring and drawing, and each row's shortcut
comes from the command spec already in hand instead of a registry
lookup.
2026-10-23 13:44:06 +00:00
raykholo ed6e123d54 Sketch: offer Break Offset Association for offsets of lines (#4899) (#4920)
* claim: #4899

* Sketch: offer Break Offset Association for offsets of lines (#4899)

Trim and Extend refuse an associated offset output and point to Break
Offset Association, but the right-click menu offered it only for
free-form offsets. Offer it whenever the curve is an offset output
(analytic, topology or free-form), using the sketch's shared lookup.
2026-10-23 13:44:04 +00:00
raykholo dad28abecf solidcraft-cli bench-assistant: the standard Assistant model test (#4851) (#4930)
* claim: #4851

* solidcraft-cli bench-assistant: the standard Assistant model test

Runs the eleven v1 tasks of #4851 against a desktop app that is already
running with a control port. Each task starts a new design and a new chat
with All permissions, runs only the prescribed setup, sends the prompt
once and waits for the turn. The result is checked on the design
(document.inspect with measurements): body count, volume within 1%
(0.3% for the fillet), bounding-box size in any orientation, the lowest
corner, the body name, live feature kinds, and changed or unchanged
content; the volume question is checked on the reply. Failures get the
issue's categories from the conversation's items. Prints a table, and
writes the issue's comment template (--markdown) and the raw results
with each conversation (--json).
2026-10-23 13:44:02 +00:00
Jasper van Loenen d1f5d6e223 History: drag features across the marker in one step, clearer drop feedback (#4938)
Dropping a feature just behind the history marker now rolls it back, and
dropping it just before the marker makes it active again, in both the
bottom strip and the left panel. timeline.reorder and timeline.move_many
take an optional `active` param that sets the marker in the same command,
so the move is one undo step and one recompute. A position on the wrong
side of the marker for the requested `active` is refused.

Drop feedback:
- bottom strip: the drop line sits centred between two features (it
  used to touch the previous one) and shows at the first position
  (it used to be drawn outside the strip there)
- left panel: a drop slot below the marker when it is at the end
- no drop line when the marker is dragged back to where it is
- a dragged feature is outlined in the drop line's green
- grab cursor on the marker, grabbing cursor while dragging the marker
  or a feature

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:44:00 +00:00
Logan Night 1b215b521d icon_audit: compare the requested-icons manifest with CRLF checkouts ignored (#4932) (#4960)
* claim: #4932

* icon_audit: compare the requested-icons manifest with CRLF checkouts ignored (#4932)

On Windows, Git for Windows checks out requested_icons.txt with CRLF line
endings (core.autocrlf; the repo has no .gitattributes), while the test
builds the manifest with \n, so the comparison fails although the lists
are the same. Read the file with \r\n normalised to \n, as #1931 did for
the icon tools. Regenerating still writes \n.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:56 +00:00
raykholo 36a354e3a7 Agent chat: serve the bundled skills' reference through an MCP tool (#4966) (#4970)
* claim: #4966

* Agent chat: serve the bundled skills' reference through an MCP tool (#4966)

The bundled skills start with "read reference.md", but SolidCraft's own
chat turns the agent's file tools off, so its agent could not follow them.

The MCP server gains a read-only `skill_reference` tool that returns a
skill's documents from copies built into the binary. It takes a skill
name and a file name from a fixed list, never a path, and refuses an
unknown skill or file with the available ones. The skills point to it,
and docs/mcp.md and skills/README.md describe it.
2026-10-23 13:43:54 +00:00
Taha Zarif a05a0f1053 Fix/mcp stdio bounded request lines (#4972)
* test(mcp): reproduce whitespace bypass of request-size limit

Add a source-level regression that rejects a JSON-RPC request larger than
MAX_LINE even when most of it is whitespace, plus bounded-stdio framing
coverage and invalid UTF-8 recovery.

* fix(mcp): bound stdio line buffering and validate raw request size

Consume oversize JSON-RPC lines in bounded memory while keeping the
following request available. Reject whitespace-padded lines before
trimming, preventing a request-size cap bypass.

* docs(mcp): keep server framing comment idiomatic
2026-10-23 13:43:52 +00:00
HiroKenEz 60efbe39e3 Sketch drag: a linkage with redundant parallelogram constraints still moves (#4585) (#4983)
* claim: #4585

* Sketch drag: a linkage held by redundant parallelograms still moves (#4585)

Cursor motion looks for the sketch's freedom in the null space of the
constraint Jacobian. Redundant constraints (a parallelogram held both
parallel and equal on each pair of sides) are dependent only at an exact
solution; after a correction to the solver's tolerance their rows differ
by about 1e-9, which the 1e-9 pivot tolerance counted as rank, so the
null space came out empty and a one-DoF linkage stayed locked. Use the
rank analysis' 1e-7, so the drag sees the freedom the sketch reports.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:49 +00:00
raykholo 9e20a373ca Browser: Shift+click above the anchor selects the whole range (#4991) (#4997)
* claim: #4991

* Browser: Shift+click above the anchor selects the whole range (#4991)

The browser rebuilds its row order while drawing, and a Shift click looked
the anchor up in that half-built list, so an anchor further down was not
there yet and only the clicked row was selected. Resolve the range against
the previous frame's complete row order instead.
2026-10-23 13:43:47 +00:00
Daniel 2f55a39c9c Timeline: delete the selected features, Shift+click ranges, delete after the history marker (#5005) (#5006)
* claim: #5005

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

* Timeline: delete the selected features, Shift+click ranges, delete after the history marker (#5005)

Delete in a feature's menu deletes the selected features when the
right-clicked one is among them ("Delete 3 Selected Features").
Shift+click selects the range from the last feature clicked (or the last
selected one), in the order taken before drawing. The menu of a feature
after the history marker deletes every feature after the marker, with
their number in the label. All of it goes through delete_features: one
timeline.delete and one undo step. The confirmation no longer lists
features that are themselves being deleted.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:45 +00:00
Jackson Oursland 80fd2fbbf1 Assistant: add Anthropic-compatible API agents (#5015) (#5017)
* claim: #5015

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

* Assistant: add Anthropic-compatible API agents (#5015)

The Add agent wizard now offers Anthropic-compatible endpoints beside OpenAI-compatible ones. The API backend speaks the Anthropic Messages protocol with the same MCP bridge, tool gate, limits, undo and keychain rules: x-api-key and anthropic-version, a top-level system note, input_schema tools, content-block answers and one user tool_result message per round. Test sends a short message because compatible endpoints (Qwen's) need not list models.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 13:43:41 +00:00
raykholo c3d1ce8237 Opening a design: range-check integer-valued coordinates too (#5021) (#5026)
* claim: #5021

* Opening a design: range-check integer-valued coordinates too (#5021)

The file-level number check skips every integer-valued number so that ids
and counters may be large, which let a coordinate such as
9223372036854775808 through. The typed design is now checked as well:
float fields (coordinates, sizes) are serialized as floats and must be
within the same +-1e9 limit, while integer fields stay integers and are
not limited.
2026-10-23 13:43:40 +00:00
raykholo 567bc50723 Snap fit: refuse sizes too small to build (#5022) (#5027)
* claim: #5022

* Plastic features: refuse sizes under 0.01 mm (#5022)

A snap fit with a near-zero thickness (1e-9 mm) got past the size checks,
which only asked for a positive number, and panicked in the kernel's
intersection code. The boss, lip, groove and rest had the same gap.

Every length of a plastic feature that becomes geometry (snap fit length,
thickness, width, catch_depth and catch_length; boss diameter, height,
hole_diameter, hole_depth and rib sizes; lip and groove width and height;
rest width, length, height and thickness) must now be at least 0.01 mm.
The check sits where the feature is evaluated, so a parameter changed later
is refused the same way, and the message names the parameter.
2026-10-23 13:43:38 +00:00
raykholo 1a5fcd05d6 Extrude and loft: refuse duplicate profile indexes (#5023) (#5028)
* claim: #5023

* Extrude and loft: refuse repeated profile indexes (#5023)

`profiles: [0, 0, ...]` with 10 000 copies built 10 000 identical bodies in
about 35 s, and a loft section listing one profile 10 000 times took 25 s
before it was refused.

A list of profile indexes now refuses a repeated index while it is parsed
("profile 0 is listed twice"), as offset association ids already were.
Before the profiles are resolved, a selection naming more profiles than its
sketch has, an index the sketch doesn't have, or two points in the same
profile is refused too. A design file that repeats an index still opens,
using the profile once.
2026-10-23 13:43:36 +00:00
Jackson Oursland 3a6153d18f Add .vscode to .gitignore. (#5588) 2026-10-23 12:49:05 -04:00
Cyryl Sochacki d273c45de6 i18n(pl): translate 213 missing messages (#5031)
Catch up with en.json: the new Preferences pages, assistant settings,
3D mouse axis assignment, bug report form, history names and the tool
help texts. pl now covers every message of en.json.
2026-10-23 13:43:32 +00:00
dudedagger42 da46c660df Coincident on two circles lays the second on the first (#5038)
Coincident with two circles (or arcs) picked was refused: "two curves must both
be straight lines; otherwise choose a point and a curve". It now lays the second
pick on the first: Concentric and Equal together, in one step, with the first
staying put. A pair already concentric or equal gets only the other half; one
already lying on the other is refused as nothing to add.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:30 +00:00
vdirienzo 92d1331978 A renamed body keeps its number: the next body does not take the freed name (#4957) (#5053)
* A renamed body keeps its number (#4957)

The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.

The design now saves the names a rename freed, and the counter skips
them.

* A renamed body keeps its number (#4957)

The design's body counter starts over at every evaluation, so a body
renamed after a join (the join changed it, so the rename cannot be
followed without evaluating the design again) gave its number back: the
next body took the name the rename freed, and a script that names a body
found another one.

The design now saves the names a rename freed, and the counter skips
them.

* Format
2026-10-23 13:43:28 +00:00
dudedagger42 01bbc0aa9f Parameters: a favourite renames by double-click (#4952) (#5070)
A favourite is listed twice, under Favourites and under User. Double-clicking its name opened a
name editor in both rows, under one widget id; the two took each other's focus, one lost it at
once, and the rename closed with the name unchanged. The rename now opens only in the table it
was started in, under an id that includes that table.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:26 +00:00
raykholo dbbad6d4ee view.apply: keep the camera's roll, and refuse an up axis (#5087) (#5094)
* claim: #5087

* view.apply: keep the camera's roll, and refuse an up axis (#5087)

An explicit camera was read as a named view, which has no roll or up, so
roll was silently reset to 0 and up was ignored. Read roll as a finite
number in radians, clamped to ±π, and refuse up with a message saying the
up axis comes from the design settings.
2026-10-23 13:43:24 +00:00
raykholo 4f69e6540c Dragging a docked dialog no longer highlights the view under it (#5086) (#5095)
A docked dialog is placed from the previous frame's drag offset, so a fast
drag can leave the pointer outside the dialog, over the model. The viewport
then treated the pointer as hovering the model and highlighted faces and
edges under it.

drag_dock now marks the frame in egui temp data while its title band is
dragged, and navigation::handle treats the pointer as not hovering the view
while that mark is current and a button is held. This covers every dialog
docked through drag_dock.
2026-10-23 13:43:23 +00:00
ivisaf 55608665ef gpu: open the OpenGL regression device without indirect-call validation (#5101)
NVIDIA's EGL answers wgpu's request for a GL 3.3 context with exactly 3.3
(GLSL 3.30) yet lists GL_ARB_compute_shader. wgpu-hal then reports compute
shaders, wgpu-core builds its indirect-call validation pipeline, naga cannot
emit compute GLSL below 4.30, and request_device fails as Device(Lost).
The test draws nothing indirect, so it opts out of that validation.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:20 +00:00
raykholo 0fb4a3e868 GPU tessellation: an empty plan no longer fails buffer validation (#5049) (#5104)
* claim: #5049

* GPU tessellation: an empty plan no longer fails buffer validation (#5049)

Empty tessellation inputs were padded to 16 bytes, smaller than the
shader's 32-byte vertex and 48-byte surface records, so wgpu refused the
dispatch. Skip the dispatch when there is nothing to evaluate, and pad
each empty buffer to one whole element of what the shader reads.
2026-10-23 13:43:18 +00:00
Christian Hammacher 2dc31a30d4 Extract to File: move a component into its own linked design (#5113)
The component's sketches, features, bodies, subcomponents, the joints among
them and the parameters they use move to a new .solidcraft design, keeping
their ids and names, and the component becomes linked to it as Insert Design
makes it. The new design must build the same bodies and the assembly must
look the same afterwards, else nothing changes and no file is written. Joint
faces on the component are found again on the linked bodies, and components
that build the same bodies can become placements of the link.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:16 +00:00
sthifen 8b68eb7eac fix(extrude): a sketch on an inserted part joins and cuts it, through all too (#5115)
A STEP file inserted into a new design sits in its own component under the
empty root, and a sketch on its face must join that part going out and cut it
going in rather than make a separate body beside it. With a distance, main
already resolves this; Through All with no distance gave `auto` no reach to
probe, so it fell back to New Body and cut nothing. Through All now reaches
past every body in its direction, and `auto` reads it as a cut (#1234).

Co-authored-by: Steven <steven2021067613@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:43:14 +00:00
bherbruck 96679f3e2b Themes: drop the logo's fourth colour, which #5465 retired (#5548)
The 14 new built-in themes still listed four logo colours, so none of them read after #5465 and main's theme test failed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:39:30 -04:00
froglemonade2 3ba17bb517 App icon: draw the borderless mark everywhere (#5461) (#5465)
* claim: #5461

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

* App icon: draw the borderless mark everywhere (#5461)

app_icon.rs drew the older mark with white lines between the faces and a
white outline around it, so the Windows, Linux, web and README icons did
not match the macOS icon, the social card or the title bar. It now draws
the borderless mark from the faces and gradients of the macOS artwork
(03-solid-blue-cutout.svg): five faces with transparent gaps between them.
The filler fills even-odd spans so the concave faces work.

The Linux hicolor PNGs and both Windows .ico files are regenerated with
`cargo xtask app-icons`. The README logo, the web app's icons and every
package pick them up from there.

The logo colour token's fourth entry (the white edge) is gone: nothing
draws it any more.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:40:09 -04:00
N3MI-DG ae3b36ac88 Agent cursor follows the agent's own pointer input (ui.move, ui.click, ui.drag, ui.scroll) (#5162)
The agent cursor only acted out engine commands: pointer input sent over the control channel
reached the app as synthetic events without moving it, so an agent's clicks, right-clicks and
drags were invisible to a person watching. While the cursor is shown, each synthetic pointer event
now moves it to where the input lands, a press leaves a ripple, and a drag is followed step by
step. A request being acted out keeps the pointer; with the cursor hidden nothing changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:42:59 +00:00
raykholo 5367ef4367 Extrude without a profile selection keeps holes in nested loops (#5172) (#5176)
* claim: #5172

* Extrude without profiles keeps nested loops as holes (#5172)

With no `profiles`, solid.extrude took every region of the sketch, so
circles drawn inside a plate outline were extruded as well and filled the
plate's holes. The default now applies even-odd nesting to the drawn
profiles: a loop inside an outline is its hole, a loop inside that hole is
material again, and separate outlines all extrude. Sketches without nesting
keep the `all` selection, and explicit `profiles` (including "all") are
unchanged. Saved designs store their selection, so they recompute as before.
2026-10-23 13:42:55 +00:00
raykholo caa1e6e196 component.create refuses parameters it does not take (#5174) (#5178)
* claim: #5174

* component.create refuses parameters it does not take (#5174)

A call with `bodies` (or a misspelled key) used to succeed and make an empty
component. Unknown keys are now refused before anything changes, the error
lists the accepted parameters, and `bodies` points to component.move_bodies
and component.from_bodies.
2026-10-23 13:42:53 +00:00
raykholo 8bd17cea42 Joints: a joint origin's name given as a face says to pass it as the origin (#5175) (#5179)
* claim: #5175

* Joints: a joint origin's name given as a face says to pass it as the origin (#5175)

When the name in `face` or `circle` matches no face but is a joint origin,
the error now says so instead of "no face is named".
2026-10-23 13:42:51 +00:00
takuto-NA 4256775274 Joint: avoid redundant analysis and residual placement work (#5185) (#5186)
* claim: #5185

* Joint: reuse residual placements and skip discarded mobility reports (#5185)
2026-10-23 13:42:49 +00:00
Jackson Oursland 5962f56f87 Viewport: reuse still pictures and draw scenes into a 1x window pass (#4820) (#5187)
* Viewport: show the picture it drew last when nothing changed (#4820)

A frame redrew the whole scene whenever egui asked for a frame, even when
the camera, scene, highlight and overlays were identical: pointer moves over
empty space and UI animations over a still view paid for the full scene.

The renderer now hashes everything the picture depends on (scene keys,
camera, size, section, images, analysis, accessibility map, occlusion) and
blits the last offscreen picture when the hash matches. The first still
frame filters a supersampled picture down once and later frames copy it as
it is. A frame with the same content costs a blit instead of a scene draw.

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

* Viewport: draw the scene offscreen, into a 1x window pass (#4820)

The scene keeps the app's depth, stencil and MSAA, but egui's window pass now
has one sample and no depth. Whenever the two targets differ, the scene
(product previews too) is drawn offscreen and blitted, so the UI pass no
longer clears and resolves a multisampled window.

Over 8 s of pointer moves on the 450E rotor head, the app's GPU intervals fell
from 1325 ms to 1006 ms and egui_render from 892 ms to 509 ms; idle stays at
0%. A GPU test checks the split path's picture matches the direct one.

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

* 3D mouse: a repeated report at rest asks for no frame (#4820)

A device may repeat its latest report while the cap rests. Publishing it
again asked egui for a frame although nothing changed. Only a report that
changes the reading now asks for one; while the cap moves, the view asks for
the next frame itself.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 13:42:47 +00:00
github-actions[bot] b6a45b8278 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-23 14:19:26 +00:00
N3MI-DG 8ae90f9667 Home: a recent design's location shows in one tooltip, not two (#5208)
The File view's recent row already shows its path on hover; Home added
a second hover text with the raw path, so both appeared.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:42:43 +00:00
William Dunn 17f0d6bc68 Format registry: one table of formats, extensions and options (#5215) (#5217)
* Add the format registry: one table of formats, extensions and options in crates/io.

The hosts' Open-dialog filters and the web accept list derive from it, and
registry tests pin it to the path predicates and the command docs.

* Fix format-registry tests that compared FormatSpec without Eq.

assert_eq on Option<&FormatSpec> does not compile; the none cases
only need is_none, matching the other rows that compare ids.
2026-10-23 13:42:40 +00:00
raykholo 8b1a722f06 Viewport: the ground shadow no longer sends every frame through the transparency path (#5230) (#5255)
* claim: #5230

* Viewport: draw the ground shadow on the direct path (#5230)

The ground shadow went into the model's translucent triangles, so every
frame with a body counted as see-through and took the order-independent
transparency path. Give it its own batch, drawn alpha-blended after the
opaque geometry as before, so only truly see-through bodies use that path.
2026-10-23 13:42:38 +00:00
takuto-NA 63d81cc096 Motion Trace: regenerate kept curves only when inputs change (#5248) (#5270)
* claim: #5248

* feat(motion): regenerate kept traces when source inputs change

* feat(motion): add a parametric Chebyshev trace demo

* perf(joints): reuse face measurements across motion samples

Face-centred joint origins called Body::faces on every solve, bypassing the
reference-mesh reuse used for rigid placements. Share face measurements with
unchanged model geometry at the existing tolerance, while guarding replaced
bodies and applying offsets after the cached measurements.

Cover repeated named/nearest-face solves, shape edits, retained snapshots and
direct replacement with deterministic work counts. The 145-point Chebyshev
dimension and range updates retain identical sampled and interpolated curves.

* perf(naming): skip preparation for cached face names

Both face lookup paths spawned fresh workers over every body even when their name cells were ready. Prepare only cold cells, with direct handling of zero or one cold body. Preserve the existing parallel naming path and all face selection semantics.

Deterministic regressions cover repeated picked and unique-name lookups, misses, partial warmup, renames, snapshots and moved copies. Release Chebyshev medians fall from 859 to 50 ms for a dimension edit and 871 to 34 ms for a sweep edit; all 145 samples and interpolated points remain identical. Full cargo xtask ci passes with RUST_TEST_THREADS=4 and the existing local Windows validation overlays.
2026-10-23 13:42:36 +00:00
raykholo 46a8f86e80 web-offline-test: wait only for the files the browser can ask for at once (#5287) (#5294)
* claim: #5287

* web-offline-test: wait only for the files the browser can ask for at once

The fail-fast step waited for every other file of build v2 to reach the
server. Chrome sends at most six requests to one server at a time, and
the page's failure cancels the download, with it the files still waiting
for a connection. A real build keeps nine files, so only six were ever
asked for and the Pages job failed with "only 6 of 8 slow files were
asked for" whenever the installing worker didn't happen to ask again.

Wait for as many as can be in flight (less the page's connection), and
give the stand-in app more files than that, so the default run covers it.
2026-10-23 13:42:33 +00:00
Mateusz Sych 94279fe059 Web: Ctrl+Tab and Ctrl+Shift+Tab switch browser tabs again (#5227) (#5350)
* claim: #5227

* Web: leave Ctrl+Tab and Ctrl+Shift+Tab to the browser (#5227)

eframe cancels the default action of every Tab keydown on its canvas
(egui moves focus with Tab), so browsers that let a page cancel their
shortcuts, such as Vivaldi, could not switch tabs while SolidCraft had
the keyboard. A capture listener on the window now stops Ctrl+Tab and
Ctrl+Shift+Tab before they reach the canvas, without cancelling them,
so the browser acts on them as usual. Plain Tab and Shift+Tab still
move between the app's fields. Desktop builds are unchanged.

web-api-test checks it in headless Chrome: Ctrl+Tab and Ctrl+Shift+Tab
keydowns are not cancelled, Tab still is.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-23 13:42:27 +00:00
raykholo 867df396ea Agent chat in the Flatpak says why no agent is found and how to allow one (#4765) (#4769)
* claim: #4765

* Agent chat: explain the Flatpak sandbox when no agent is found (#4765)

In the Flatpak the sandbox has no home-folder or network access, so agents
installed through nvm or ~/.local/bin are never discovered and the chat only
said that no agent was found. With FLATPAK_ID set and nothing discovered, the
picker and Preferences > AI now say why and offer the exact
`flatpak override --user ai.storyteller.solidcraft ...` command with a Copy
button. The message is in every catalog, and docs/agent-chat.md gains a
Flatpak section listing what each permission is for. The manifest is
unchanged.
2026-10-23 13:42:14 +00:00
JEREMY 858e5995d6 Plane at Angle: a planar face is a base plane (#1939) (#1956)
Co-authored-by: jeremymiribung-blip <jeremymiribung-blip@users.noreply.github.com>
2026-10-23 11:32:24 +00:00
bherbruck edf57cec29 Stand up Y-up STEP and IGES files from SolidWorks automatically (#5191)
* claim: #5150

* Stand it up: don't offer it for SolidCraft's own STEP files (#5150)

STEP replies from file.insert_step and doc.open now carry the file's
originating system, and the window skips the stand-up offer when it is
exactly SolidCraft, as SolidCraft's STEP export writes it.

* Stand up Y-up STEP and IGES files from SolidWorks automatically

The STEP header (originating system, preprocessor version, FILE_DESCRIPTION) and the IGES
global section's native system ID say which program wrote a file. A SolidWorks header means
Y up: the import's component is stood up as it opens, as one undo step, and the message offers
Undo. SolidCraft's own files are left alone; an unknown exporter keeps the offer.

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

---------

Co-authored-by: Ray <raykholo@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 02:53:20 +00:00
raykholo 0c72ad56d2 An idle window draws nothing with the frame-rate readout on (#5165)
The viewport's frame-rate readout asked for a new frame every frame, so with
it on an untouched window redrew as fast as the display allowed and kept the
GPU busy: 77% GPU at 60 fps on an M1 with a 5K display showing the sample,
against 0% with the readout off. It now asks for no frames of its own and
shows how often the view really draws; one frame a little over a second after
the frames stop shows the view idle at 0 FPS. The preference's note no longer
says the view redraws continuously.

The status toast also asked for a frame every half second for the six
seconds it shows; it now asks for one frame, when it is due to go.
2026-10-23 02:19:37 +00:00
raykholo 92d3a2fde5 GPU: a lost device stops drawing instead of crashing (#4172) (#4229)
* claim: #4172

* GPU: a lost device stops drawing instead of crashing (#4172)

After the driver resets the GPU (Windows TDR), the first queue write to meet the
lost device panicked inside wgpu-core (PendingWrites::activate unwrapping
DeviceError::Lost). Nothing watched the device for its loss.

- egui-wgpu: RenderState::device_lost (DeviceLost) is set by the device's lost
  callback. The painter draws nothing on a lost device, and before a frame's
  uploads it submits nothing (DeviceLost::probe), so a loss the driver reports
  is found by that submission instead of the first write.
- Pictures (thumbnails of saves, ui.render) watch the same flag: on a lost
  device the capturer refuses with GpuError::DeviceLost without touching it,
  and the picture comes from the CPU rasterizer. Headless capturers watch
  their own devices.
- The app, seeing the loss, closes keeping unsaved changes for recovery (as a
  forced quit does) and starts again; the new start says what happened. A
  second loss within ten minutes starts on the software renderer. The loss is
  noted in the crash journal.
2026-10-23 02:19:35 +00:00
Jackson Oursland b0048a6104 Mesh bodies: measure their size once, when made (#4662) (#4663)
`Body::size` walked a mesh body's vertices on every call, and it is asked
for every frame. A mesh body's triangles never change after `mesh_body` or
`mesh_transformed` makes it, so both now measure it then (`TriMesh::span`)
and `size` returns it like a B-rep body's. `ModelBody::mesh` returns a mesh
it already has before working out the display tolerance.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:51:40 -04:00
raykholo 0db2abbeca Viewport legend and readouts stay above the floating History card (#4107) (#4129)
* claim: #4107

* Keep bottom viewport overlays above the floating History card (#4107)

In the floating layout the History card covered the analysis legend and
the frame-rate readout (#3896). One bottom inset, computed from the
History card (zero when History is hidden or docked), now positions every
bottom-anchored viewport overlay: the surface-analysis and Continuity
legends, the frame-rate readout, the command-progress chip and the
navigation bar fallback.
2026-10-23 00:48:27 +00:00
bherbruck 9761d75699 Release: a failed snap build no longer holds up the release (#5168)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:50:38 -04:00
bherbruck a06bdf5cfc Release 0.20.0
The tree is main at 030187452 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-22 18:34:40 -04:00
bherbruck 030187452a Release 0.20.0 (#5118)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:40:01 -04:00
ralfkuh-lab 198fbeb814 i18n: every UI label reaches the catalog, on the UI kit (#2483)
On main, 120 label literals (95 distinct) can't be translated in any
locale: the English catalog doesn't reach them. Catalogue them in en,
ja, zh and de, translate indented row labels once for display and
measuring, look up the thread profile by its id, and give each
navigation preset's help, shown by the mouse mapping picker, a catalog
message.

A guard tokenizes the UI sources and checks every label literal of a
kit call, of context_menu::button and of tr_label/tr/SelInput against
the English catalog, resolving kit calls by their imports.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:08:05 +00:00
Adam Hickerson 186096cbe6 agent chat: agent detection finishes on the start page (#3265) 2026-10-22 20:52:13 +00:00
Colin Faulkingham 3b982ca371 Prevent deleted item IDs from being reused (#1016) (#3641)
* claim: #1016

* Prevent deleted design item IDs from being reused
2026-10-22 18:31:39 +00:00
Loohi aaa110218b Edit Offset Plane: keep a face base, refuse a plane on itself (#3567) (#3889)
* claim: #3567

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

* Edit Offset Plane: keep a face base, refuse a plane on itself (#3567)

Reopening an Offset Plane built on a face looked the face up at the
plane's origin, which can lie off the face, so the Plane input came up
empty. Look it up at the point picked on the face. A redefine that
builds a feature on itself is refused with a plain message instead of
failing with "plane offsets nested too deeply".

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:30:02 +00:00
raykholo 46f80b769f Sketch fillet and chamfer refuse an offset's corners instead of leaving circles (#2874) (#3860)
* claim: #2874

* Sketch fillet and chamfer refuse an offset's corners (#2874)

An analytic offset's output ends are recomputed from its source curves on
every solve and held fixed there. Filleting an output corner moved those
ends to the tangent points, the next refresh pulled them back to the sharp
corner, and the new arcs closed into full circles with the sketch left
unsolved.

Fillet and chamfer now refuse a corner on an offset output before anything
changes (a batch fillet applies all of its corners or none) and say how to
get the rounded result: round the source corners before offsetting, or
break the offset association first, after which the same corners round
normally.
2026-10-22 18:21:13 +00:00
raykholo 26ca82735b Circular Pattern and Revolve: a construction axis can be the axis (#2885, #4506) (#3796)
* claim: #2885

* Circular Pattern and Revolve: a construction axis can be the axis (#2885)

The dialogs send a picked axis by its name, but solid.pattern.circular
only knew X, Y and Z, and solid.revolve / surface.revolve read any other
name as a sketch line id. Picking a construction axis therefore failed.

- solid.pattern.circular takes a construction axis name, saves it as
  `axis_feature` and turns about the axis as evaluated, so the copies
  follow it when it is edited.
- AxisRef::Construction { name } for revolve and surface revolve,
  resolved at evaluation and checked to lie in the sketch plane.
- construct.plane.at_angle accepts a construction axis name (its fixed
  axis line is taken from the axis as it is now).
- Reopening these features shows the construction axis as the pick.
- Design format: pending/construction-axis-refs.rs.

* Construction axis references follow the axis in the feature's component frame (#4506)

- Revolve, Surface Revolve and Circular Pattern resolve a construction axis
  where it is evaluated, mapped into the feature's component frame.
- A line of the revolved sketch with the same id as a construction axis
  still wins, so existing scripts keep their meaning.
- Tests: the revolve keeps the reference through save and reopen and
  follows an edited axis (60π to 80π mm³); perpendicular, offset and
  profile-crossing axes are refused without changing the design.
2026-10-22 18:21:11 +00:00
SS-EN 408d30ea56 Joint drive moves parts without evaluating them (#3391) (#3397)
* Joint drive moves parts without evaluating them

A joint drive (also the Drive dialog and motion steps) changes only occurrence transforms, yet it
re-evaluated nearly the whole timeline: every feature that might read another component's
geometry (a sketch with any link, even to its own axis) hashed every occurrence's transform into
its fingerprint, so any motion invalidated it and everything after it, and no new pose was ever
in the cache.

- Placement digest: a feature now hashes only where the geometry it actually reads lies relative
  to its own frame (link sources, anchors, profiles and paths from other components, mirror
  placements; To Object and face/edge-picked planes read every component, relatively). Moving
  parts that keep those relative placements leaves every result in place: nothing is evaluated
  or re-meshed. In-context features whose referenced part really moved still rebuild.
- Evaluated ahead, applied once: `Ahead::evaluate` returns what it evaluated and
  `Session::prime` lets the window-thread pass take it (history commands and dialog OK), so an
  off-thread command no longer costs twice on designs past the cache's budget.
- Contact meshes are kept per body geometry across drives.

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

* Joint drive: booleans read only their input bodies' placements

A join, cut or intersect with named targets, and a Combine, read every
placement, so a joined part transferred to a component re-ran its boolean
at each new joint pose. Each now hashes only the placements of the bodies
it actually takes as inputs, resolved in its timeline input state (before
any later BodyTransfer), relative to the frame that evaluates it; Combine
reads its tools relative to its target. Unresolved and legacy component or
path reads keep the conservative dependency. 24 new joint_pose_ tests
cover new angles, transfers, targets, save/reopen, undo and real relative
motion (900 -> 800 mm3).

Supplement offered on #3397 by takuto-NA.

Co-Authored-By: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Creating a component recomputes nothing

A new, empty component changes nothing a feature reads, yet it changed
the placement fingerprint of every targeted boolean, Combine and To
Object feature, so `component.create` evaluated the timeline again:
- the first component of a design: `placement_digest` returned 0 while
  the design had no placements and a nonzero value once it had one;
- To / From Object and legacy face- or edge-picked planes hashed every
  component, not just those holding the bodies, sketches and
  construction geometry the feature can reach.

Co-Authored-By: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test: creating a component after a legacy face sketch recomputes nothing

A face extruded after its construction certificate is gone (a patterned
cut, a pad joined flush with it, then one more cut) is sketched on a
legacy face plane, which reads every placed body. A new, empty component
must not change what it reads either (reported by Tyler Chiriboga).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: takuto-NA <183465875+takuto-NA@users.noreply.github.com>
Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
2026-10-22 18:21:08 +00:00
Jackson Oursland 75261cd797 Viewport: order-independent transparency for see-through surfaces (#4734) (#4903)
* Viewport: levels of detail for large meshes (#4724)

A large instanced mesh (20 k triangles or more) gets coarser copies, made
once on a worker (`mesh_lod`): a quarter, a sixteenth and a sixty-fourth of
its triangles, by quadric edge collapse (`geom::simplify`, after Garland and
Heckbert 1997; no fold-overs, no sheets joined, open boundaries kept), each
with its measured deviation from the full mesh. Each frame, as placements
are culled, each is drawn from the coarsest copy whose deviation stays
under 0.75 px where it stands (`placement_scale`, `level_for`). Until the
copies are made the mesh is drawn in full; the scene is built again when
they are. Display only: picking, measuring and exports keep the reference
mesh. Outside the kernel.

25 placements of a 2 M-triangle STL, 3600 x 2190 px, 4x MSAA, M2 Max
(`instanced_scene_gpu_benchmark`): 20.2 -> 1.2 ms a frame with all in
view, 21.9 -> 2.9 ms close up. The copies take 10 s to make, on a worker.

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

* Viewport: see-through and faded parts drawn as instances (#4888)

See-through (an appearance's opacity) and faded (context around an active
component) bodies in one colour are drawn as instances like opaque ones,
their alpha in each placement's colour: one copy of the triangles, levels
of detail and culling, no per-placement copies built on the window thread.
Their placements are ordered back to front each frame (not their
triangles one by one) for devices that sort see-through surfaces.

The GPU benchmark gains `SOLIDCRAFT_BENCH_OPACITY` and the CPU share of
each frame, and its pictures each placement's own triangles too.

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

* Viewport: order-independent transparency for see-through surfaces (#4734)

The model's see-through surfaces (glass, faded context; instanced or not)
were sorted back to front triangle by triangle on the window thread
whenever the view turned. They are now drawn with weighted blended order-
independent transparency (McGuire and Bavoil 2013): when there are any,
the frame is drawn into the renderer's own textures (as supersampling
does), the see-through surfaces gathered into a weighted colour and a
revealage target against the frame's depth, laid over the frame in one
full-screen pass, then the overlays drawn over them. Nothing is sorted.
A device that cannot blend into float targets keeps the sorted way.

25 placements of a 2 M-triangle STL at opacity 0.5, M2 Max, 3600 x 2190 px:
about 10 s a frame sorted, 21.6 ms (17.2 ms instanced, 2.8 ms with levels
of detail).

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 16:17:39 +00:00
Tyler Chiriboga 6f5d0038ab 3D mouse: find devices on Windows without asking every HID device for its strings (#2614) (#3653)
* claim: #2614 (3D mouse: find devices on Windows without asking every HID device for its strings)

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

* 3D mouse: find devices on Windows without asking every HID device for its strings (#2614)

On Windows the 3D mouse reader rescanned every two seconds while no 3D
mouse was plugged in, with hidapi's refresh_devices(). hidapi's Windows
enumeration opens every HID interface and reads its serial number,
manufacturer and product strings: on USB, three control requests to
every keyboard, mouse and touchscreen, forever, on a machine without a
3D mouse. That fits the report of an idle machine whose USB keyboard
drops off and returns and whose cursor jumps.

Windows now asks Plug and Play which present HID interfaces are
multi-axis controllers (hardware ID HID_DEVICE_UP:0001_U:0008), which
sends nothing to any device, and opens only those with open_path().
hidapi's own discovery is turned off before the reader makes its one
context, so making it touches no device either. macOS keeps hidapi's
enumeration (IOKit's registry); Linux is unchanged.

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

* AGENTS.md: list the Plug and Play scan among the files allowed unsafe code (#2614)

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

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-22 15:44:27 +00:00
bherbruck 1814adb06a Leaving a sketch keeps the camera where it is (#4909)
Finish Sketch, double Esc (including a blank sketch discarded), and Cancel no longer
restore the pre-sketch view. Entering a sketch still turns the view to face the plane.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 11:04:04 -04:00
Nathan Dane 971b28c982 claim: preferences redesign (#4608) 2026-10-22 13:53:36 +00:00
Nathan Dane f79cbc216c Redesign the home page as the File view (#2027) (#3822)
* claim: #2027

* Redesign the home page as the File view (#2027)

* Match the File view with the accepted component design (#2027)

* Restore the Start subtitle and simplify the recovery icon (#2027)

* Integrate File view with main's shared UI containers
2026-10-22 12:19:09 +00:00
Jackson Oursland 4dc1828138 Imported meshes: share their triangles between document copies (#4673) (#4675)
Every edit clones the document for undo, and `MeshData`'s positions and
triangles (plain `Vec`s) were copied with it: ~165 MB a command for a 2 M-
triangle STL. They are now `Arc<[f64]>` and `Arc<[u32]>`, shared by clones;
the few places that rewrite them build new arrays. Saved designs are
unchanged (an `Arc<[T]>` serializes as the array).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:06:44 +00:00
Jackson Oursland f843a3f83c Picking: remove the GPU pick buffer, the per-body BVHs answer alone (#4665) (#4666)
The pick buffer drew the model's surfaces and edge ids for every still
view to narrow which edges a pick tests. Measured against the BVHs alone
(release, M2 Max, 1400 x 900 view, 600 cursor positions), a pick costs
2-51 us through the BVHs and the tile saved 0-4 us of it, while each pass
cost 0.7-12.5 ms of GPU time per still view, plus gathering its scene on
the window thread, read-backs a frame or two late and repaints to wait for
them (and, before, a copy of every triangle, which made it refuse models
past 6 M triangles). Picks are unchanged: the BVHs give the same answers.

`pick_timing_on_curved_models` gains eight layers of bodies seen from above
(the pick buffer's best case) and `SOLIDCRAFT_BENCH_MESH` placements.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:37 +00:00
Jackson Oursland f0fd5c10b7 Viewport: keep a face's hover highlight while the pointer moves across it (#4668) (#4669)
The highlight's key hashed the hovered item with its point under the
pointer, so every move across a face built and uploaded its highlight
again: for an imported mesh (one face) all its triangles, every frame.
The key now leaves out a face's and a plane's point, which the highlight
does not draw; an edge's middle stays in (it picks the part lit).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:33 +00:00
Jackson Oursland 981d23d1d2 Viewport: weld a large mesh's shared vertices quicker, and ahead of its first frame (#4679) (#4680)
`SharedTris::new` used the standard SipHash hasher and collected each
triangle's corners into a Vec: 451 ms for a 2 M-triangle STL on the window
thread. A quick hasher for the coordinates' bits and a fixed array make it
278 ms, and the background mesher that meshes an opened or inserted
file's bodies now welds their large meshes too (`warm_shared_tris`), so the
first scene that shows them takes the shared vertices ready-made.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:28 +00:00
Jackson Oursland 01648da72d Ribbon: work out each tab's panel lists once (#4658) (#4659)
`toolbar_custom::panel_lists` is asked for every frame and matched every
registered command against every section by string each time (~337 us a
call for MODEL, several calls a frame). The registry and the ribbon layout
never change while the app runs, so each tab's lists are kept: 0.2 us a call.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 12:03:24 +00:00
Jackson Oursland 3d3dd27439 Viewport: leave out placements of an instanced mesh that are out of view (#4646) (#4647)
* claim: #4616

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

* Viewport: draw a part placed many times from one copy of its triangles (#4616)

A part placed by many occurrences was drawn, framed and picked from a moved
copy of its mesh per placement: on the CPU (the moved mesh and its scene
bytes) and on the GPU (its vertices uploaded once per placement).

- The model scene draws bodies that share a mesh as GPU instances: the
  shared triangles once, a rigid move and colour per placement
  (`InstancedMesh`, `vs_tri_inst`), in the solid, section and AO passes.
  `GpuScene::merged` expands them, so scene tests see world triangles.
- `ModelBody::mesh_in_place`, `plan_mesh_in_place_at`, `mesh_bounds` and
  `display_identity` give the shared mesh, its placement, the exact box and
  an identity without making the moved copy.
- Display refinement's body ids, Fit/Home bounds, the viewport's body boxes,
  the background mesher and picking use them (picking moves the ray into
  the part's frame and shares one pick index).

25 placements of a 2 M-triangle STL on an M2 Max: orbit 45 -> 37 ms a
frame, footprint 41 -> 16 GB, 24 copies 39 -> 18 s.

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

* Viewport: draw large meshes from shared, indexed vertices (#4637)

An instanced mesh's vertices are shared between its triangles and drawn
indexed (`SharedTris`). A flat-faceted mesh (an STL's facets) shares them by
position alone and lights each fragment with its triangle's own plane
(`vs_tri_inst_flat`, `fs_*_flat`), taken from the fragment's position
relative to the eye so it stays exact at any zoom; its vertex outputs are
kept few, since a tiled GPU stores every vertex's outputs. A body alone is
drawn this way from 50 k triangles. A mesh too large for one buffer falls
back to unshared vertices.

`instanced_scene_gpu_benchmark` (manual, ignored): 25 placements of a 2 M-
triangle STL at 3600 x 2190 px, 4x MSAA, M2 Max: 21.3 ms a frame for the
placements' own triangles, 16.1 ms shared and indexed.

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

* Viewport: leave out placements of an instanced mesh that are out of view (#4646)

Each frame `Batches::cull` keeps the placements whose box may be seen
(`placed_box_in_view`: all eight corners past one side of the view in clip
space, so it holds behind the eye too) and writes them again only when
that set changes. The manual benchmark gains `SOLIDCRAFT_BENCH_CLOSE=1`:
close up on the middle of 25 placements of a 2 M-triangle STL (13 in
view), 3600 x 2190 px, 4x MSAA, M2 Max: 28.2 ms a frame for the
placements' own triangles, 17.2 ms instanced and culled.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 04:04:59 +00:00
bherbruck 6c60f1a9ed web-offline-test: hold responses until the test lets them go (#4620)
The slow steps waited on the clock (6 s sleeps, a 1 s timer, a 30 s cap), so on a busy
runner a launch could outlast them and time out at 240 s. The server now holds a response
until the test releases it, and the test waits on events:

- overlapping downloads: build v5's own files are held; files both builds share (index.html,
  names no build changes) are served, since the browser queues a request behind an in-flight
  one for the same URL. The held files are released once v6 has started.
- fail-fast step: the other files wait until the test has seen them asked for.
- update in another tab: the other tab updates once the launch is waiting for build.json, and
  the browser's own sw.js check waits for the same moment.
- hung requests no longer give up after 20 s.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 21:51:01 -04:00
343 changed files with 27635 additions and 4052 deletions
+2
View File
@@ -203,6 +203,8 @@ jobs:
name: Snap ${{ matrix.arch }}
needs: [version, linux]
if: needs.version.outputs.build == 'true'
# The Snap Store hasn't accepted SolidCraft yet, so a snap failure never holds up a release.
continue-on-error: true
# Repackages the Linux job's tarball, like the Flatpak. On 24.04 because the core24 base is
# Ubuntu 24.04, which lets snapcraft build on the runner itself (--destructive-mode).
runs-on: ${{ matrix.runner }}
+1
View File
@@ -7,6 +7,7 @@
/.claude/worktrees/
/target-snapshot
__pycache__/
.vscode/
# cargo-mutants output (docs/mutation-testing.md)
mutants.out*/
+4 -2
View File
@@ -33,9 +33,11 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
and calls through its function pointers), two functions in
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
records crashes inside native code such as graphics drivers) and the Windows-only
records crashes inside native code such as graphics drivers), the Windows-only
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
test of the window without OS decorations).
test of the window without OS decorations) and the Windows-only
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
mice, so no other device is sent anything).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
returns an error ("not supported yet: …"); it never panics.
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
+8 -1
View File
@@ -14,6 +14,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| File | Author | Source | Licence |
|---|---|---|---|
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 |
@@ -107,7 +108,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -196,6 +197,11 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_keyboard.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_monitor.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_mouse.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_palette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/settings_plug.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -336,6 +342,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/flat/send.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
Generated
+56 -17
View File
@@ -996,6 +996,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
@@ -2271,6 +2280,16 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "keyboard-types"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
dependencies = [
"bitflags 2.13.2",
"serde",
]
[[package]]
name = "keyring"
version = "3.6.3"
@@ -2480,6 +2499,24 @@ dependencies = [
"pxfm",
]
[[package]]
name = "muda"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
dependencies = [
"crossbeam-channel",
"dpi",
"keyboard-types",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"png",
"thiserror 2.0.21",
"windows-sys 0.61.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -4162,7 +4199,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4172,6 +4209,7 @@ dependencies = [
"hidapi",
"keyring",
"log",
"muda",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
@@ -4193,11 +4231,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.19.0"
version = "0.21.0"
[[package]]
name = "solidcraft-cli"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"image",
"log",
@@ -4209,7 +4247,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"proc-macro2",
"quote",
@@ -4218,7 +4256,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"log",
"serde",
@@ -4235,7 +4273,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"log",
"schemars",
@@ -4254,7 +4292,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"serde",
"serde_json",
@@ -4263,7 +4301,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"log",
"naga",
@@ -4280,7 +4318,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4303,7 +4341,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"log",
"proptest",
@@ -4323,7 +4361,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4332,7 +4370,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"image",
"moxcms",
@@ -4346,7 +4384,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"serde",
"serde_json",
@@ -4357,14 +4395,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"arboard",
"egui",
@@ -4373,6 +4411,7 @@ dependencies = [
"log",
"minisign-verify",
"pollster 0.4.0",
"proc-macro2",
"semver",
"serde",
"serde_json",
@@ -4391,7 +4430,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"eframe",
"egui",
@@ -5921,7 +5960,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.19.0"
version = "0.21.0"
dependencies = [
"proc-macro2",
"serde_json",
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.19.0"
version = "0.21.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+5
View File
@@ -70,6 +70,11 @@ pointer movement orbits without a mouse button, and vertical scrolling zooms. Ot
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
action tiles, recent rows and sample cards, with recovery review and Save As/Export
entry points. Document loading, sample caches and recovery safety retain their
existing command paths.
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
+698 -22
View File
@@ -1,6 +1,6 @@
Third-party software in SolidCraft
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 563 crates below.
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
@@ -95,6 +95,7 @@ core_detect 1.0.0 (MIT/Apache-2.0)
cpufeatures 0.2.17 (MIT OR Apache-2.0)
cpufeatures 0.3.1 (MIT OR Apache-2.0)
crc32fast 1.5.2 (MIT OR Apache-2.0)
crossbeam-channel 0.5.17 (MIT OR Apache-2.0)
crossbeam-deque 0.8.8 (MIT OR Apache-2.0)
crossbeam-epoch 0.9.21 (MIT OR Apache-2.0)
crossbeam-utils 0.8.23 (MIT OR Apache-2.0)
@@ -210,6 +211,7 @@ jni-sys 0.4.1 (MIT OR Apache-2.0)
jni-sys-macros 0.4.1 (MIT OR Apache-2.0)
js-sys 0.3.106 (MIT OR Apache-2.0)
katexit 0.1.5 (MIT OR Apache-2.0)
keyboard-types 0.8.3 (MIT OR Apache-2.0)
keyring 3.6.3 (MIT OR Apache-2.0)
khronos-egl 6.0.0 (MIT/Apache-2.0)
kurbo 0.13.1 (Apache-2.0 OR MIT)
@@ -234,6 +236,7 @@ minisign-verify 0.3.0 (MIT)
miniz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0)
miniz_oxide 0.9.1 (MIT OR Zlib OR Apache-2.0)
moxcms 0.8.1 (BSD-3-Clause OR Apache-2.0)
muda 0.21.2 (Apache-2.0 OR MIT)
multiversion_no_op 1.0.0 (Apache-2.0 OR MIT)
naga 30.0.1 (MIT OR Apache-2.0)
naga-types 30.0.1 (MIT OR Apache-2.0)
@@ -1108,26 +1111,27 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
4.7.1, atomic-waker 1.1.2, base64 0.23.1, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo
3.20.3, cfg-if 1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
1.0.0, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, dbus-secret-service
4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14,
event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1, gltf-derive
1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna
1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106,
khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14,
log 0.4.34, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46,
num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0,
parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5,
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
security-framework-sys 2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2,
smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1,
unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi
0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures
0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129,
web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys
0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1,
linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8,
num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19,
once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5,
parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling
3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust
1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width
0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
wit-bindgen 0.57.1
================================================================================
Apache License
@@ -4030,7 +4034,605 @@ Apache License
limitations under the License.
================================================================================
crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
crossbeam-channel 0.5.17
================================================================================
===============================================================================
matching.go
https://creativecommons.org/licenses/by/3.0/legalcode
Creative Commons Legal Code
Attribution 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the Work and
other pre-existing works, such as a translation, adaptation,
derivative work, arrangement of music or other alterations of a
literary or artistic work, or phonogram or performance and includes
cinematographic adaptations or any other form in which the Work may be
recast, transformed, or adapted including in any form recognizably
derived from the original, except that a work that constitutes a
Collection will not be considered an Adaptation for the purpose of
this License. For the avoidance of doubt, where the Work is a musical
work, performance or phonogram, the synchronization of the Work in
timed-relation with a moving image ("synching") will be considered an
Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as
encyclopedias and anthologies, or performances, phonograms or
broadcasts, or other works or subject matter other than works listed
in Section 1(f) below, which, by reason of the selection and
arrangement of their contents, constitute intellectual creations, in
which the Work is included in its entirety in unmodified form along
with one or more other contributions, each constituting separate and
independent works in themselves, which together are assembled into a
collective whole. A work that constitutes a Collection will not be
considered an Adaptation (as defined above) for the purposes of this
License.
c. "Distribute" means to make available to the public the original and
copies of the Work or Adaptation, as appropriate, through sale or
other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or entities that
offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic work,
the individual, individuals, entity or entities who created the Work
or if no individual or entity can be identified, the publisher; and in
addition (i) in the case of a performance the actors, singers,
musicians, dancers, and other persons who act, sing, deliver, declaim,
play in, interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the producer
being the person or legal entity who first fixes the sounds of a
performance or other sounds; and, (iii) in the case of broadcasts, the
organization that transmits the broadcast.
f. "Work" means the literary and/or artistic work offered under the terms
of this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book,
pamphlet and other writing; a lecture, address, sermon or other work
of the same nature; a dramatic or dramatico-musical work; a
choreographic work or entertainment in dumb show; a musical
composition with or without words; a cinematographic work to which are
assimilated works expressed by a process analogous to cinematography;
a work of drawing, painting, architecture, sculpture, engraving or
lithography; a photographic work to which are assimilated works
expressed by a process analogous to photography; a work of applied
art; an illustration, map, plan, sketch or three-dimensional work
relative to geography, topography, architecture or science; a
performance; a broadcast; a phonogram; a compilation of data to the
extent it is protected as a copyrightable work; or a work performed by
a variety or circus performer to the extent it is not otherwise
considered a literary or artistic work.
g. "You" means an individual or entity exercising rights under this
License who has not previously violated the terms of this License with
respect to the Work, or who has received express permission from the
Licensor to exercise rights under this License despite a previous
violation.
h. "Publicly Perform" means to perform public recitations of the Work and
to communicate to the public those public recitations, by any means or
process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a
place individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of the
performances of the Work, including by public digital performance; to
broadcast and rebroadcast the Work by any means including signs,
sounds or images.
i. "Reproduce" means to make copies of the Work by any means including
without limitation by sound or visual recordings and the right of
fixation and reproducing fixations of the Work, including storage of a
protected performance or phonogram in digital form or other electronic
medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
limit, or restrict any uses free from copyright or rights arising from
limitations or exceptions that are provided for in connection with the
copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License,
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
perpetual (for the duration of the applicable copyright) license to
exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more
Collections, and to Reproduce the Work as incorporated in the
Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation,
including any translation in any medium, takes reasonable steps to
clearly label, demarcate or otherwise identify that changes were made
to the original Work. For example, a translation could be marked "The
original work was translated from English to Spanish," or a
modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated
in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme cannot be waived, the Licensor
reserves the exclusive right to collect such royalties for any
exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme can be waived, the Licensor waives the
exclusive right to collect such royalties for any exercise by You
of the rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right to
collect royalties, whether individually or, in the event that the
Licensor is a member of a collecting society that administers
voluntary licensing schemes, via that society, from any exercise
by You of the rights granted under this License.
The above rights may be exercised in all media and formats whether now
known or hereafter devised. The above rights include the right to make
such modifications as are technically necessary to exercise the rights in
other media and formats. Subject to Section 8(f), all rights not expressly
granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly made
subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms
of this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms
on the Work that restrict the terms of this License or the ability of
the recipient of the Work to exercise the rights granted to that
recipient under the terms of the License. You may not sublicense the
Work. You must keep intact all notices that refer to this License and
to the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or Publicly
Perform the Work, You may not impose any effective technological
measures on the Work that restrict the ability of a recipient of the
Work from You to exercise the rights granted to that recipient under
the terms of the License. This Section 4(a) applies to the Work as
incorporated in a Collection, but this does not require the Collection
apart from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any Licensor You
must, to the extent practicable, remove from the Collection any credit
as required by Section 4(b), as requested. If You create an
Adaptation, upon notice from any Licensor You must, to the extent
practicable, remove from the Adaptation any credit as required by
Section 4(b), as requested.
b. If You Distribute, or Publicly Perform the Work or any Adaptations or
Collections, You must, unless a request has been made pursuant to
Section 4(a), keep intact all copyright notices for the Work and
provide, reasonable to the medium or means You are utilizing: (i) the
name of the Original Author (or pseudonym, if applicable) if supplied,
and/or if the Original Author and/or Licensor designate another party
or parties (e.g., a sponsor institute, publishing entity, journal) for
attribution ("Attribution Parties") in Licensor's copyright notice,
terms of service or by other reasonable means, the name of such party
or parties; (ii) the title of the Work if supplied; (iii) to the
extent reasonably practicable, the URI, if any, that Licensor
specifies to be associated with the Work, unless such URI does not
refer to the copyright notice or licensing information for the Work;
and (iv) , consistent with Section 3(b), in the case of an Adaptation,
a credit identifying the use of the Work in the Adaptation (e.g.,
"French translation of the Work by Original Author," or "Screenplay
based on original Work by Original Author"). The credit required by
this Section 4 (b) may be implemented in any reasonable manner;
provided, however, that in the case of a Adaptation or Collection, at
a minimum such credit will appear, if a credit for all contributing
authors of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits for the
other contributing authors. For the avoidance of doubt, You may only
use the credit required by this Section for the purpose of attribution
in the manner set out above and, by exercising Your rights under this
License, You may not implicitly or explicitly assert or imply any
connection with, sponsorship or endorsement by the Original Author,
Licensor and/or Attribution Parties, as appropriate, of You or Your
use of the Work, without the separate, express prior written
permission of the Original Author, Licensor and/or Attribution
Parties.
c. Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any
Adaptations or Collections, You must not distort, mutilate, modify or
take other derogatory action in relation to the Work which would be
prejudicial to the Original Author's honor or reputation. Licensor
agrees that in those jurisdictions (e.g. Japan), in which any exercise
of the right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion, mutilation,
modification or other derogatory action prejudicial to the Original
Author's honor and reputation, the Licensor will waive or not assert,
as appropriate, this Section, to the fullest extent permitted by the
applicable national law, to enable You to reasonably exercise Your
right under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this License.
Individuals or entities who have received Adaptations or Collections
from You under this License, however, will not have their licenses
terminated provided such individuals or entities remain in full
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is
perpetual (for the duration of the applicable copyright in the Work).
Notwithstanding the above, Licensor reserves the right to release the
Work under different license terms or to stop distributing the Work at
any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is
required to be, granted under the terms of this License), and this
License will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
offers to the recipient a license to the original Work on the same
terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and
enforceable.
d. No term or provision of this License shall be deemed waived and no
breach consented to unless such waiver or consent shall be in writing
and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with
respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that
may appear in any communication from You. This License may not be
modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention
for the Protection of Literary and Artistic Works (as amended on
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
and the Universal Copyright Convention (as revised on July 24, 1971).
These rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be enforced
according to the corresponding provisions of the implementation of
those treaty provisions in the applicable national law. If the
standard suite of rights granted under applicable copyright law
includes additional rights not granted under this License, such
additional rights are deemed to be included in the License; this
License is not intended to restrict the license of any rights under
applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not be
liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder, it
shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of doubt,
this trademark restriction does not form part of this License.
Creative Commons may be contacted at https://creativecommons.org/.
===============================================================================
The Go Programming Language
https://golang.org/LICENSE
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
===============================================================================
The Rust Programming Language
https://github.com/rust-lang/rust/blob/master/LICENSE-MIT
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
===============================================================================
The Rust Programming Language
https://github.com/rust-lang/rust/blob/master/LICENSE-APACHE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================================
crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
================================================================================
The MIT License (MIT)
@@ -7716,6 +8318,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
keyboard-types 0.8.3
================================================================================
Copyright (c) 2017 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
keyring 3.6.3
================================================================================
@@ -9452,6 +10078,56 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
muda 0.21.2
================================================================================
MIT License
Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
muda 0.21.2
================================================================================
SPDXVersion: SPDX-2.1
DataLicense: CC0-1.0
PackageName: muda
DataFormat: SPDXRef-1
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
PackageHomePage: https://tauri.app
PackageLicenseDeclared: Apache-2.0
PackageLicenseDeclared: MIT
PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy
PackageSummary: <text>Menu Utilities for Desktop Applications.
</text>
PackageComment: <text>The package includes the following libraries; see
Relationship information.
</text>
Created: 2022-12-05T09:00:00Z
PackageDownloadLocation: git://github.com/tauri-apps/muda
PackageDownloadLocation: git+https://github.com/tauri-apps/muda.git
PackageDownloadLocation: git+ssh://github.com/tauri-apps/muda.git
Creator: Person: Daniel Thompson-Yvetot
================================================================================
multiversion_no_op 1.0.0
================================================================================
+700
View File
@@ -0,0 +1,700 @@
//! `bench-assistant`: the standard Assistant model test (v1, #4851), run against a desktop app.
//!
//! The app is already running with `--control PORT`; everything goes through its control channel
//! (`docs/control-protocol.md`), so the command needs no window code of its own. For each task it
//! starts a new design, runs the task's prescribed setup (a box, for some), adds the endpoint as an
//! API agent with All permissions, starts a new chat, sends the prompt once and waits for the turn
//! to end. The result is checked on the design (`document.inspect` with measurements), never on
//! what the model says, except for the one question whose answer is the reply. A failed task is
//! put in one of the issue's failure categories from the conversation's items.
//!
//! ```text
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT --base-url URL --model NAME
//! [--token T] [--tasks 1,2,5] [--repeat N] [--timeout-s 300]
//! [--json out.json] [--markdown out.md]
//! ```
//! prints a table of the results; `--markdown` writes the issue's comment template filled in,
//! `--json` every task's rules, conversation and design before and after.
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpStream};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
/// Standard test v1, as #4851 states it. Changing a prompt or a rule makes it another test.
const TASKS: &str = r#"[
{"id": "01-cube", "prompt": "Make a 20 mm cube.", "check": {"bodies": 1, "volume": 8000, "bbox_size": [20, 20, 20]}},
{"id": "02-cylinder", "prompt": "Make a cylinder 30 mm in diameter and 50 mm tall.", "check": {"bodies": 1, "volume": 35342.92, "bbox_size": [30, 30, 50]}},
{"id": "03-block", "prompt": "Create a block 60 mm long, 40 mm wide and 10 mm thick.", "check": {"bodies": 1, "volume": 24000, "bbox_size": [10, 40, 60]}},
{"id": "04-sketch-extrude", "prompt": "Sketch a 50 by 30 mm rectangle on the XY plane and extrude it 15 mm.", "check": {"bodies": 1, "volume": 22500, "bbox_size": [15, 30, 50], "feature_kinds": ["Extrude"]}},
{"id": "05-plate-hole", "prompt": "Make a plate 80 x 60 x 10 mm with a 20 mm diameter hole straight through the middle.", "check": {"bodies": 1, "volume": 44858.41, "bbox_size": [10, 60, 80], "note": "80*60*10 - pi*10^2*10"}},
{"id": "06-fillet-box", "prompt": "Round all the edges of the box with a 3 mm radius.", "setup": [{"command": "solid.box", "params": {"length": 40, "width": 40, "height": 40}}], "check": {"bodies": 1, "volume": 63108.53, "tol": 0.003, "feature_kinds": ["Fillet"], "note": "all 12 edges of a 40 mm cube rounded r=3"}},
{"id": "07-move-body", "prompt": "Move the box 50 mm along the X axis.", "setup": [{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 20}}], "check": {"bodies": 1, "volume": 8000, "bbox_min": [50, 0, 0]}},
{"id": "08-rename-body", "prompt": "Rename the body to Bracket.", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "body_name": "Bracket"}},
{"id": "09-volume-question", "prompt": "What is the volume of the part?", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "unchanged": true, "reply_number": "6[ ,. ]?000(?![0-9])|6(\\.0+)? ?cm"}},
{"id": "10-two-holes", "prompt": "Make a 100 x 50 x 5 mm plate and drill two 10 mm holes through it, each 15 mm in from the short ends and centred across the width.", "check": {"bodies": 1, "volume": 24214.6, "bbox_size": [5, 50, 100], "note": "100*50*5 - 2*pi*5^2*5"}},
{"id": "11-any-change", "prompt": "can you use the software to make a change to the current shape, any change at all", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"changed": true, "note": "verbatim user prompt from #4846; pass = the design changed"}}
]"#;
/// The agent the test adds. A fixed id replaces it on every run instead of adding another.
const AGENT_ID: &str = "api-bench-assistant";
const AGENT_NAME: &str = "bench";
/// Longest control reply read (the channel's own request limit).
const MAX_REPLY: usize = 16 << 20;
/// Most result data kept for `--json`.
const MAX_RESULTS: usize = 64 << 20;
/// Deadline of the requests that only set up or read (each is answered at once by the app).
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
/// How often a running turn is polled.
const POLL: Duration = Duration::from_millis(250);
/// After this long, an idle chat with no reply yet counts as a turn that ended without one.
const START_GRACE: Duration = Duration::from_secs(30);
/// Absolute tolerance of the lowest corner, in millimetres.
const MIN_CORNER_TOL: f64 = 0.05;
/// A JSON-lines connection to the app. Requests are sent once; a lost reply is never retried,
/// so nothing runs twice.
struct Control {
addr: SocketAddr,
token: String,
reader: BufReader<TcpStream>,
id: u64,
}
impl Control {
fn connect(addr: &str, token: Option<&str>) -> Result<Self, String> {
let addr: SocketAddr = addr.parse().map_err(|_| format!("--connect: `{addr}` is not an IP address and port"))?;
if !addr.ip().is_loopback() || addr.port() == 0 {
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
}
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
if token.len() > 4096 {
return Err("the control token is longer than 4096 bytes".into());
}
Self::open(addr, token)
}
fn open(addr: SocketAddr, token: String) -> Result<Self, String> {
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| format!("connecting to {addr}: {e}"))?;
Ok(Self { addr, token, reader: BufReader::new(stream), id: 0 })
}
/// A new connection to the same app: a request that outlived its deadline keeps the old one
/// busy until it is answered.
fn reopen(&self) -> Result<Self, String> {
Self::open(self.addr, self.token.clone())
}
fn call(&mut self, method: &str, params: Value, deadline: Instant) -> Result<Value, String> {
let left = || deadline.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()).ok_or_else(|| "timeout".to_string());
let io = |e: std::io::Error| match e.kind() {
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timeout".to_string(),
_ => format!("control channel: {e}"),
};
self.reader.get_ref().set_write_timeout(Some(left()?)).map_err(io)?;
self.id += 1;
// The first request on a connection carries the token; sending it on every one is allowed.
let request = json!({"id": self.id, "method": method, "params": params, "token": self.token});
writeln!(self.reader.get_mut(), "{request}").map_err(io)?;
let mut line = Vec::new();
loop {
self.reader.get_ref().set_read_timeout(Some(left()?)).map_err(io)?;
let bytes = self.reader.fill_buf().map_err(io)?;
if bytes.is_empty() {
return Err("the app closed the control connection".into());
}
let newline = bytes.iter().position(|b| *b == b'\n');
let used = newline.map_or(bytes.len(), |n| n + 1);
if line.len() + used > MAX_REPLY {
return Err("a control reply is larger than 16 MiB".into());
}
line.extend_from_slice(bytes.get(..used).unwrap_or_default());
self.reader.consume(used);
if newline.is_some() {
break;
}
}
let reply: Value = serde_json::from_slice(&line).map_err(|e| format!("control reply: {e}"))?;
if reply["id"].as_u64() != Some(self.id) {
return Err("control reply for another request".into());
}
if reply["ok"] != true {
return Err(format!("{method}: {}", reply["error"].as_str().unwrap_or("failed")));
}
reply.get("result").cloned().ok_or_else(|| format!("{method}: reply without a result"))
}
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String> {
self.call("engine.execute", json!({"command": command, "params": params, "animate": false}), Instant::now() + REQUEST_TIMEOUT)
}
fn chat(&mut self, params: Value) -> Result<Value, String> {
self.call("ui.agent_chat", params, Instant::now() + REQUEST_TIMEOUT)
}
fn inspect(&mut self) -> Result<Value, String> {
let design = self.call("document.inspect", json!({"measure": true}), Instant::now() + REQUEST_TIMEOUT)?;
if design.is_object() { Ok(design) } else { Err("document.inspect returned no design".into()) }
}
}
fn tasks() -> Result<Vec<Value>, String> {
serde_json::from_str(TASKS).map_err(|e| format!("embedded tasks: {e}"))
}
fn triple(value: &Value) -> Option<[f64; 3]> {
match value.as_array()?.as_slice() {
[x, y, z] => {
let n = |v: &Value| v.as_f64().filter(|n| n.is_finite());
Some([n(x)?, n(y)?, n(z)?])
}
_ => None,
}
}
/// What the model said (thoughts left out).
fn reply(items: &[Value]) -> String {
items.iter().filter(|i| i["thought"] != true).filter_map(|i| i["agent"].as_str()).collect::<Vec<_>>().join("\n")
}
/// v1's reply pattern `6[ ,. ]?000(?![0-9])|6(\.0+)? ?cm`: "6000", "6 000", "6,000", "6.000" not
/// followed by a digit, or "6 cm", "6.0cm", … (a narrow no-break space also separates thousands).
fn volume_reply(text: &str) -> bool {
text.match_indices('6').any(|(at, _)| {
let tail = text.get(at + 1..).unwrap_or_default();
let thousands = |t: &str| t.strip_prefix("000").is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_digit()));
let separated = tail.strip_prefix([' ', ',', '.', '\u{202f}']).is_some_and(thousands);
let cm = {
let t = tail.strip_prefix(".0").map_or(tail, |t| t.trim_start_matches('0'));
t.strip_prefix(' ').unwrap_or(t).starts_with("cm")
};
thousands(tail) || separated || cm
})
}
/// The design as the user sees it, without what reading or saving changes (path, saved state,
/// revision, selection, undo depth, evaluation timings).
fn content(design: &Value) -> Value {
let mut v = design.clone();
if let Some(o) = v.as_object_mut() {
for key in ["path", "dirty", "revision", "selection", "undo", "redo"] {
o.remove(key);
}
}
for feature in v.get_mut("timeline").and_then(Value::as_array_mut).into_iter().flatten() {
if let Some(o) = feature.as_object_mut() {
o.remove("ms");
o.remove("boolean_diagnostics");
}
}
v
}
/// Whether the design changed: its content, or a step on the undo stack (a colour, say, that the
/// inspection does not list).
fn changed(before: &Value, after: &Value) -> bool {
content(before) != content(after) || after["undo"].as_u64() > before["undo"].as_u64()
}
/// The rules a task's result breaks (none: it passed). `before` and `after` are
/// `document.inspect {measure: true}` before the prompt and after the turn.
pub(super) fn check(rule: &Value, before: &Value, after: &Value, items: &[Value]) -> Vec<String> {
let Some(all) = after["bodies"].as_array() else { return vec!["the design could not be read".into()] };
// Bodies the user sees; a hidden one (a tool body, a hidden original) is not the result.
let bodies: Vec<&Value> = all.iter().filter(|b| b["visible"] != false).collect();
let mut errors = Vec::new();
if let Some(n) = rule["bodies"].as_u64()
&& bodies.len() as u64 != n
{
errors.push(format!("{} bodies, expected {n}", bodies.len()));
}
if let Some(expected) = rule["volume"].as_f64() {
let tol = rule["tol"].as_f64().unwrap_or(0.01);
let volumes: Vec<f64> = bodies.iter().filter_map(|b| b["volume_mm3"].as_f64()).filter(|v| v.is_finite()).collect();
let total: f64 = volumes.iter().sum();
if volumes.len() != bodies.len() || (total - expected).abs() > expected * tol {
errors.push(format!("volume {total:.1} mm³, expected {expected} mm³ ±{}%", tol * 100.0));
}
}
let first = bodies.first().copied().unwrap_or(&Value::Null);
let min = triple(&first["bbox"]["min"]);
if let Some(mut expected) = triple(&rule["bbox_size"]) {
expected.sort_by(f64::total_cmp);
let size = min.zip(triple(&first["bbox"]["max"])).map(|(lo, hi)| {
let mut size = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
size.sort_by(f64::total_cmp);
size
});
if !size.is_some_and(|s| s.iter().zip(expected).all(|(a, e)| (a - e).abs() <= 0.01 * e.max(1.0))) {
errors.push(format!("size {size:?}, expected {expected:?} in any orientation"));
}
}
if let Some(expected) = triple(&rule["bbox_min"])
&& !min.is_some_and(|m| m.iter().zip(expected).all(|(a, e)| (a - e).abs() <= MIN_CORNER_TOL))
{
errors.push(format!("lowest corner {min:?}, expected {expected:?}"));
}
if let Some(name) = rule["body_name"].as_str()
&& !bodies.iter().any(|b| b["name"] == name)
{
errors.push(format!("no body named {name}"));
}
for kind in rule["feature_kinds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
let live = |f: &&Value| f["suppressed"] != true && f["rolled_back"] != true && f["error"].is_null();
if !after["timeline"].as_array().into_iter().flatten().filter(live).any(|f| f["type"].as_str().is_some_and(|t| t.contains(kind))) {
errors.push(format!("no {kind} in the timeline"));
}
}
if rule["changed"] == true || rule["unchanged"] == true {
let changed = before.is_object() && changed(before, after);
if rule["changed"] == true && !changed {
errors.push("the design did not change".into());
}
if rule["unchanged"] == true && (changed || !before.is_object()) {
errors.push("the design changed".into());
}
}
if rule["reply_number"].is_string() && !volume_reply(&reply(items)) {
errors.push("the reply does not give the volume".into());
}
errors
}
/// A tool call written into the reply as JSON: `{"name": …`, `{"command": …`, `{"function": …`,
/// `{"tool": …`.
fn call_as_text(text: &str) -> bool {
text.match_indices('{').any(|(at, _)| {
let rest = text.get(at + 1..).unwrap_or_default().trim_start();
["\"name\"", "\"command\"", "\"function\"", "\"tool\""]
.iter()
.any(|key| rest.strip_prefix(key).is_some_and(|r| r.trim_start().starts_with(':')))
})
}
/// How a failed task failed, in the issue's words, from the conversation's items.
pub(super) fn classify(items: &[Value], timed_out: bool) -> &'static str {
if timed_out {
return "timeout";
}
let tools: Vec<&Value> = items.iter().filter(|i| i.get("tool").is_some()).collect();
// A failed call, or a batch whose answer reports a failed step (`"ok": false`).
let failed: Vec<String> = tools
.iter()
.map(|t| t["output"].as_str().unwrap_or_default())
.zip(&tools)
.filter(|(output, t)| t["status"] == "Failed" || serde_json::from_str::<Value>(output).is_ok_and(|v| v["ok"] == false))
.map(|(output, _)| output.to_lowercase())
.collect();
let notices: Vec<String> = items.iter().filter_map(|i| i["notice"].as_str()).map(str::to_lowercase).collect();
let answer = reply(items);
if notices.iter().any(|n| n.contains("http 400")) {
return "the endpoint refused tools (HTTP 400)";
}
if notices.iter().any(|n| n.contains("max_turn_requests")) || answer.contains("requests for one message") {
return "hit the request limit";
}
if call_as_text(&answer) {
return "wrote the call as text";
}
if failed.iter().any(|o| o.contains("unknown command") || o.starts_with("unknown tool")) {
return "wrong or unknown command";
}
if failed.iter().any(|o| o.contains("invalid parameters") || o.starts_with("not run:") || o.contains("not valid json")) {
return "wrong arguments";
}
if !failed.is_empty() || notices.iter().any(|n| n.starts_with("http ")) || items.iter().any(|i| i["error"] == true) {
return "gave up after errors";
}
if tools.is_empty() {
return if answer.trim().is_empty() { "an empty reply" } else { "only talked" };
}
"wrong result"
}
/// One run's results in the issue's comment template (`run`: which repetition).
pub(super) fn markdown(model: &str, version: &str, results: &[Value], run: u64) -> String {
let clean = |s: &str| s.replace(['\n', '\r', '|'], " ");
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
let passed = rows.iter().filter(|r| r["passed"] == true).count();
let mut out = format!(
"### {} (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: {}\nResult: {passed}/11\n| # | pass/fail | how it failed |\n|---|---|---|\n",
clean(model),
clean(version)
);
for n in 1..=11 {
match rows.iter().find(|r| r["task"] == n) {
Some(r) if r["passed"] == true => out.push_str(&format!("| {n} | pass | |\n")),
Some(r) => out.push_str(&format!("| {n} | fail | {} |\n", clean(r["failure"].as_str().unwrap_or_default()))),
None => out.push_str(&format!("| {n} | | not run |\n")),
}
}
out.push_str(if rows.len() == 11 {
"Notes: <anything odd, e.g. very slow, needed a retry>\n"
} else {
"Notes: partial run; the tasks marked \"not run\" were left out.\n"
});
out
}
fn write(path: &str, text: &str) -> Result<(), String> {
solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"))
}
struct Options<'a> {
connect: &'a str,
token: Option<&'a str>,
base_url: &'a str,
model: &'a str,
tasks: Vec<usize>,
repeat: u64,
timeout: Duration,
json: Option<&'a str>,
markdown: Option<&'a str>,
}
fn options(args: &[String]) -> Result<Options<'_>, String> {
const FLAGS: [&str; 9] = ["--connect", "--token", "--base-url", "--model", "--tasks", "--repeat", "--timeout-s", "--json", "--markdown"];
let mut given = std::collections::BTreeMap::new();
let mut args = args.iter();
while let Some(flag) = args.next() {
let Some(flag) = FLAGS.iter().find(|f| **f == flag.as_str()) else { return Err(format!("unknown bench-assistant option: {flag}")) };
let value = args
.next()
.filter(|v| !v.starts_with("--") && !v.is_empty() && v.len() <= 4096)
.ok_or_else(|| format!("{flag} needs a value (at most 4096 bytes)"))?;
if given.insert(*flag, value.as_str()).is_some() {
return Err(format!("{flag} given twice"));
}
}
let required = |key: &str| given.get(key).copied().ok_or_else(|| format!("bench-assistant needs {key}"));
let number = |key: &str, default: u64, max: u64| -> Result<u64, String> {
given
.get(key)
.map_or(Ok(default), |s| s.parse::<u64>().ok().filter(|n| (1..=max).contains(n)).ok_or_else(|| format!("{key} must be 1 to {max}")))
};
let tasks = match given.get("--tasks") {
None => (1..=11).collect(),
Some(list) => {
let mut tasks = Vec::new();
for n in list.split(',') {
let n = n
.trim()
.parse::<usize>()
.ok()
.filter(|n| (1..=11).contains(n))
.ok_or("--tasks takes task numbers 1 to 11, separated by commas")?;
if tasks.contains(&n) {
return Err(format!("--tasks lists task {n} twice"));
}
tasks.push(n);
}
tasks
}
};
Ok(Options {
connect: required("--connect")?,
token: given.get("--token").copied(),
base_url: required("--base-url")?,
model: required("--model")?,
tasks,
repeat: number("--repeat", 1, 100)?,
timeout: Duration::from_secs(number("--timeout-s", 300, 3600)?),
json: given.get("--json").copied(),
markdown: given.get("--markdown").copied(),
})
}
/// Waits for the turn the prompt started to end. Returns the chat's last state and whether the
/// deadline passed first.
fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: Instant) -> Result<(Value, bool), String> {
let mut seen_busy = false;
loop {
let state = chat["state"].as_str().unwrap_or_default();
let turn = !chat["turn"].is_null();
let queued = chat["pending"].as_array().is_some_and(|p| !p.is_empty());
seen_busy |= state == "busy" || turn;
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
// A turn can start and end between two polls: then the conversation has its answer.
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
return Ok((chat, false));
}
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
std::thread::sleep(POLL.min(left));
match control.call("ui.agent_chat", json!({}), end) {
Ok(next) => chat = next,
Err(e) if e == "timeout" => return Ok((chat, true)),
Err(e) => return Err(e),
}
}
}
pub fn run(args: &[String]) -> Result<(), String> {
let o = options(args)?;
let tasks = tasks()?;
let mut control = Control::connect(o.connect, o.token)?;
let help = control.call("ui.help", json!({}), Instant::now() + REQUEST_TIMEOUT)?;
let version = help["version"].as_str().filter(|s| !s.is_empty() && s.len() <= 256).ok_or("the app reported no version")?.to_string();
let config = control.chat(json!({
"features": true,
"add_agent": {"id": AGENT_ID, "name": AGENT_NAME, "kind": "api", "base_url": o.base_url, "model": o.model},
}))?;
if !config["agents"].as_array().into_iter().flatten().any(|a| a["id"] == AGENT_ID) {
return Err("the app did not add the benchmark's agent".into());
}
// Put the user's permission setting back afterwards.
let permissions = config["permissions"].as_str().map(str::to_string);
let result = run_tasks(&mut control, &o, &tasks, &version);
let _ = control.chat(json!({"stop": true}));
if let Some(p) = permissions {
let _ = control.chat(json!({"permissions": p}));
}
result
}
fn run_tasks(control: &mut Control, o: &Options, tasks: &[Value], version: &str) -> Result<(), String> {
let mut results: Vec<Value> = Vec::new();
let mut kept = 0;
let mut stdout = std::io::stdout().lock();
let mut say = |line: String| writeln!(stdout, "{line}").map_err(|e| format!("stdout: {e}"));
say(format!("bench-assistant v1: {} on SolidCraft {version}", o.model))?;
say("| run | # | task | pass/fail | how it failed | seconds |\n|---|---|---|---|---|---|".into())?;
for run in 1..=o.repeat {
for &n in &o.tasks {
let task = tasks.get(n - 1).ok_or("missing embedded task")?;
// A fresh design and a fresh chat, with nothing of the last task still running.
control.chat(json!({"stop": true}))?;
control.execute("file.new", json!({"name": "Benchmark"}))?;
for step in task["setup"].as_array().into_iter().flatten() {
control.execute(step["command"].as_str().ok_or("embedded setup without a command")?, step["params"].clone())?;
}
control.chat(json!({"agent": AGENT_ID, "permissions": "all", "new_chat": true, "attach_view": false}))?;
let before = control.inspect()?;
let began = Instant::now();
let end = began + o.timeout;
let (chat, timed_out) = match control.call("ui.agent_chat", json!({"send": task["prompt"]}), end) {
Ok(chat) => wait_for_turn(control, chat, began, end)?,
Err(e) if e == "timeout" => (json!({}), true),
Err(e) => return Err(e),
};
if timed_out {
// The late reply would arrive on the old connection; stop the model on a new one
// before reading the design.
*control = control.reopen()?;
}
// Stop the agent before reading the result, so nothing changes the design meanwhile.
let stopped = control.chat(json!({"stop": true}))?;
let items = stopped["items"].as_array().or(chat["items"].as_array()).cloned().unwrap_or_default();
let after = control.inspect()?;
let failed_rules = check(&task["check"], &before, &after, &items);
let passed = failed_rules.is_empty();
let failure = if passed { "" } else { classify(&items, timed_out) };
let seconds = began.elapsed().as_secs_f64();
say(format!(
"| {run} | {n} | {} | {} | {failure} | {seconds:.0} |",
task["id"].as_str().unwrap_or_default(),
if passed { "pass" } else { "fail" }
))?;
let result = json!({
"task": n, "id": task["id"], "prompt": task["prompt"], "repeat": run, "passed": passed, "failure": failure,
"failed_rules": failed_rules, "timed_out": timed_out, "seconds": seconds, "check": task["check"],
"items": items, "before": before, "after": after,
});
kept += result.to_string().len();
if kept > MAX_RESULTS {
return Err("the results passed 64 MiB; run fewer tasks or repetitions".into());
}
results.push(result);
// Written after every task, so an interrupted run keeps what it measured.
if let Some(path) = o.json {
let all = json!({"test": "v1", "model": o.model, "base_url": o.base_url, "solidcraft": version, "results": results});
write(path, &serde_json::to_string_pretty(&all).map_err(|e| e.to_string())?)?;
}
if let Some(path) = o.markdown {
write(path, &(1..=run).map(|r| markdown(o.model, version, &results, r)).collect::<Vec<_>>().join("\n"))?;
}
}
}
for run in 1..=o.repeat {
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
say(format!("run {run}: {}/{} passed", rows.iter().filter(|r| r["passed"] == true).count(), rows.len()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn design(bodies: Value, timeline: Value) -> Value {
json!({"name": "Benchmark", "params": [], "timeline": timeline, "bodies": bodies, "sketches": [], "undo": 1, "revision": 3, "dirty": true})
}
fn body(name: &str, volume: f64, min: [f64; 3], max: [f64; 3]) -> Value {
json!({"name": name, "volume_mm3": volume, "bbox": {"min": min, "max": max}, "visible": true})
}
#[test]
fn the_embedded_tasks_are_v1() {
let tasks = tasks().unwrap();
assert_eq!(tasks.len(), 11);
assert_eq!(tasks[0]["prompt"], "Make a 20 mm cube.");
assert_eq!(tasks[10]["prompt"], "can you use the software to make a change to the current shape, any change at all");
assert_eq!(tasks[5]["check"]["tol"], 0.003);
assert_eq!(tasks[6]["check"]["bbox_min"], json!([50, 0, 0]));
let boxes: Vec<_> = tasks.iter().filter(|t| t["setup"].is_array()).map(|t| t["id"].as_str().unwrap()).collect();
assert_eq!(boxes, ["06-fillet-box", "07-move-body", "08-rename-body", "09-volume-question", "11-any-change"]);
}
#[test]
fn pass_rules() {
let tasks = tasks().unwrap();
let rule = |n: usize| tasks[n - 1]["check"].clone();
let empty = design(json!([]), json!([]));
let pass = |n: usize, before: &Value, after: &Value, items: &[Value]| check(&rule(n), before, after, items);
// 3: any orientation, within 1%.
let block = design(json!([body("Body1", 24_100.0, [0.0; 3], [40.0, 10.0, 60.2])]), json!([{"type": "Box"}]));
assert!(pass(3, &empty, &block, &[]).is_empty());
let thick = design(json!([body("Body1", 24_000.0, [0.0; 3], [40.0, 12.0, 60.0])]), json!([]));
assert_eq!(pass(3, &empty, &thick, &[]).len(), 1);
let two = design(json!([body("A", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0]), body("B", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0])]), json!([]));
assert!(!pass(3, &empty, &two, &[]).is_empty());
// A hidden body is not part of the result.
let mut hidden = two.clone();
hidden["bodies"][1]["visible"] = json!(false);
hidden["bodies"][0]["volume_mm3"] = json!(24_000.0);
hidden["bodies"][0]["bbox"]["max"] = json!([60.0, 40.0, 10.0]);
assert!(pass(3, &empty, &hidden, &[]).is_empty());
// 4: the timeline needs a working Extrude.
let plate = |kind: Value| design(json!([body("Body1", 22_500.0, [0.0; 3], [50.0, 30.0, 15.0])]), kind);
assert!(pass(4, &empty, &plate(json!([{"type": "Sketch"}, {"type": "Extrude"}])), &[]).is_empty());
assert!(!pass(4, &empty, &plate(json!([{"type": "Box"}])), &[]).is_empty());
for flag in [json!({"suppressed": true}), json!({"rolled_back": true}), json!({"error": "failed"})] {
let mut feature = json!({"type": "Extrude"});
feature.as_object_mut().unwrap().extend(flag.as_object().unwrap().clone());
assert!(!pass(4, &empty, &plate(json!([feature])), &[]).is_empty(), "{flag}");
}
// 6: the fillet's tolerance is 0.3%.
let start = design(json!([body("Body1", 64_000.0, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}]));
let filleted = |v: f64| design(json!([body("Body1", v, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}, {"type": "Fillet"}]));
assert!(pass(6, &start, &filleted(63_108.53 * 1.002), &[]).is_empty());
assert!(!pass(6, &start, &filleted(63_108.53 * 1.004), &[]).is_empty());
// 7: the lowest corner.
let moved = |x: f64| design(json!([body("Body1", 8000.0, [x, 0.0, 0.0], [x + 20.0, 20.0, 20.0])]), json!([]));
assert!(pass(7, &empty, &moved(50.0), &[]).is_empty());
assert!(!pass(7, &empty, &moved(0.0), &[]).is_empty());
// 8: the name.
let named = |name: &str| design(json!([body(name, 6000.0, [0.0; 3], [30.0, 20.0, 10.0])]), json!([]));
assert!(pass(8, &empty, &named("Bracket"), &[]).is_empty());
assert!(!pass(8, &empty, &named("Body1"), &[]).is_empty());
// 9: unchanged, and the reply gives the volume.
let start = named("Body1");
let said = |text: &str| [json!({"user": "What is the volume of the part?"}), json!({"agent": text, "thought": false})];
let mut read = start.clone();
read["revision"] = json!(7);
read["dirty"] = json!(false);
read["selection"] = json!([{"body": "Body1"}]);
assert!(pass(9, &start, &read, &said("The volume is 6000 mm³.")).is_empty());
assert!(!pass(9, &start, &read, &said("About 60 cm³.")).is_empty());
assert!(!pass(9, &start, &named("Bracket"), &said("6 000 mm³")).is_empty());
let mut undone = start.clone();
undone["undo"] = json!(2);
assert!(!pass(9, &start, &undone, &said("6000 mm³")).is_empty());
// 11: any change counts; reading does not.
assert!(pass(11, &start, &named("Bracket"), &[]).is_empty());
assert!(pass(11, &start, &undone, &[]).is_empty());
assert!(!pass(11, &start, &read, &[]).is_empty());
// An unreadable design passes nothing.
assert!(!pass(11, &start, &json!({}), &[]).is_empty());
}
#[test]
fn the_volume_reply() {
for yes in ["6000", "6 000 mm³", "6,000", "6.000", "6\u{202f}000", "6 cm³", "6.00cm3", "is 6000."] {
assert!(volume_reply(yes), "{yes}");
}
for no in ["60000", "6,0001", "60 cm³", "600", "6.01 cm³", ""] {
assert!(!volume_reply(no), "{no}");
}
}
#[test]
fn failure_categories() {
for (items, expected) in [
(
json!([{"user": "Make a 20 mm cube."}, {"agent": "{\"name\": \"execute\", \"arguments\": {}}", "thought": false}]),
"wrote the call as text",
),
(
json!([{"notice": "HTTP 400: registry.ollama.ai/library/gemma3:4b does not support tools", "error": true}]),
"the endpoint refused tools (HTTP 400)",
),
(json!([{"user": "Make a 20 mm cube."}, {"agent": "", "thought": false}]), "an empty reply"),
(json!([{"agent": "Let me think", "thought": true}]), "an empty reply"),
(json!([{"agent": "Sure, here is how to make a cube.", "thought": false}]), "only talked"),
(
json!([{"tool": "1", "status": "Failed", "output": "unknown command `solid.cube` (did you mean `solid.box`?)"}]),
"wrong or unknown command",
),
(json!([{"tool": "1", "status": "Failed", "output": "unknown tool `make_cube`"}]), "wrong or unknown command"),
(json!([{"tool": "1", "status": "Failed", "output": "invalid parameters for `solid.box`: `length` is required"}]), "wrong arguments"),
(json!([{"tool": "1", "status": "Failed", "output": "Not run: the tool arguments were not valid JSON"}]), "wrong arguments"),
(json!([{"tool": "1", "status": "Failed", "output": "no body `Box`"}]), "gave up after errors"),
(
json!([{"tool": "1", "status": "Completed", "output": "{\"ok\": false, \"failed\": {\"error\": \"unknown command `solid.delete`\"}}"}]),
"wrong or unknown command",
),
(
json!([{"tool": "1", "status": "Completed"}, {"agent": "\n(stopped after 24 requests for one message; send another message to go on)"}]),
"hit the request limit",
),
(json!([{"tool": "1", "status": "Completed", "output": "{}"}]), "wrong result"),
] {
let items = items.as_array().unwrap();
assert_eq!(classify(items, false), expected, "{items:?}");
assert_eq!(classify(items, true), "timeout");
}
}
#[test]
fn markdown_is_the_comment_template() {
let rows: Vec<Value> =
(1..=11).map(|n| json!({"task": n, "repeat": 1, "passed": n != 2, "failure": if n == 2 { "an empty reply" } else { "" }})).collect();
let report = markdown("qwen2.5:7b", "0.19.0", &rows, 1);
assert!(report.starts_with(
"### qwen2.5:7b (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: 0.19.0\nResult: 10/11\n| # | pass/fail | how it failed |\n|---|---|---|\n| 1 | pass | |\n| 2 | fail | an empty reply |\n"
));
assert!(report.ends_with("| 11 | pass | |\nNotes: <anything odd, e.g. very slow, needed a retry>\n"));
let partial = markdown("m|x", "v", &rows[..1], 1);
assert!(partial.starts_with("### m x ("));
assert!(partial.contains("Result: 1/11") && partial.contains("| 2 | | not run |") && partial.contains("partial run"));
}
#[test]
fn options_are_checked_before_connecting() {
for extra in [
&["--repeat", "0"][..],
&["--repeat", "101"],
&["--timeout-s", "3601"],
&["--tasks", "0"],
&["--tasks", "12"],
&["--tasks", "1,1"],
&["--model", "again"],
&["--unexpected", "1"],
&["--token"],
] {
let mut args: Vec<String> =
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "http://127.0.0.1:11434/v1"].iter().map(|s| s.to_string()).collect();
args.extend(extra.iter().map(|s| s.to_string()));
assert!(options(&args).is_err(), "{extra:?}");
}
let args: Vec<String> =
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "u", "--tasks", "2, 5"].iter().map(|s| s.to_string()).collect();
let o = options(&args).unwrap();
assert_eq!((o.tasks, o.repeat, o.timeout), (vec![2, 5], 1, Duration::from_secs(300)));
assert!(Control::connect("10.0.0.1:9271", Some("t")).is_err());
assert!(Control::connect("localhost", Some("t")).is_err());
}
}
+10 -2
View File
@@ -6,6 +6,9 @@
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
//! the standard Assistant model test (#4851) on a running app
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
@@ -24,6 +27,7 @@
#![forbid(unsafe_code)]
mod bench_agent;
mod bench_assistant;
mod crashes;
mod fuzz;
mod logger;
@@ -57,6 +61,7 @@ const USAGE: &str = "usage:
(--ops: after opening, run each command of the script and undo it, timing the command, the
evaluations it caused and the view it leaves: placing and meshing what changed)
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]
solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]
[--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...
@@ -136,6 +141,8 @@ fn main() -> ExitCode {
Some("bench-open") => cmd_bench_open(&args[1..]),
// A ~1,300-body design built command by command, as an agent builds one over MCP.
Some("bench-agent") => bench_agent::run(&args[1..]),
// The standard Assistant model test (#4851), run against a desktop app over its control channel.
Some("bench-assistant") => bench_assistant::run(&args[1..]),
Some("--version" | "-V") => {
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
Ok(())
@@ -716,9 +723,10 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let t = std::time::Instant::now();
let s = Session::opened(doc, saved.as_ref());
drop(read);
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway).
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
// only the features whose state a thinned file left out.
let adopted = saved.is_some() && s.model.last_recomputed == 0;
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted}));
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted, "recomputed": s.model.last_recomputed}));
for r in &s.model.results {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
+82 -1
View File
@@ -7,7 +7,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::{ModelBody, ModelState};
use solidcraft_engine::doc::{ModelBody, ModelState, SavedModel};
fn corpus() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
@@ -149,6 +149,87 @@ fn imported_assemblies_open_from_their_model_as_evaluated() {
check("imported hinge", s);
}
/// `built` saved with `saved`, a model of it that keeps only some states (as a file without room
/// for all of them does, #3220), opens as the design evaluated again: evaluating just the
/// features whose state the file left out, then after suppressing a feature at, before or after
/// each state it kept, after undoing that, and with the marker moved there and back.
fn check_thinned(name: &str, built: &Session, saved: &SavedModel) {
let design = built.design();
let bytes =
solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(saved)).unwrap();
assert!(solidcraft_engine::io::read_design_geometry(&bytes).is_some(), "{name}: the file keeps the final state");
let path = temp(&name.replace(' ', "-"));
std::fs::write(&path, &bytes).unwrap();
let mut cached = open(&path);
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
let features = saved.records["features"].as_array().unwrap();
let unkept = features.iter().filter(|f| f["skipped"] == false && f.get("state").is_none()).count();
assert!(unkept > 0, "{name}: some states left out");
assert_eq!(cached.model.recomputed, unkept, "{name}: evaluated just the features whose state the file left out");
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: opened from the file's model");
let n = cached.doc.features.len();
let mut at: Vec<usize> = features
.iter()
.enumerate()
.filter(|(_, f)| f["skipped"] == false && f.get("state").is_some())
.flat_map(|(k, _)| [k.saturating_sub(1), k, k + 1])
.filter(|i| *i < n)
.collect();
at.dedup();
for i in at {
let edit = json!({"feature": cached.doc.features[i].id, "suppressed": true});
let (a, b) = (cached.execute("timeline.suppress", &edit), rebuilt.execute("timeline.suppress", &edit));
assert_eq!(a.is_ok(), b.is_ok(), "{name}: suppressing feature {i}");
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: feature {i} suppressed");
if a.is_ok() {
cached.execute("edit.undo", &json!({})).unwrap();
rebuilt.execute("edit.undo", &json!({})).unwrap();
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: suppressing feature {i} undone");
}
for marker in [json!({"position": i}), json!({})] {
cached.execute("timeline.rollback", &marker).unwrap();
rebuilt.execute("timeline.rollback", &marker).unwrap();
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: the marker at {marker}, from feature {i}");
}
}
let _ = std::fs::remove_file(&path);
}
/// A model too big for its file keeps the final state, then the slowest features' states as fit
/// (#3220). Both the smallest such model (the final state, and states that add no body to it)
/// and one that keeps more open as the design evaluated again. The placed components' design has
/// a cut whose fingerprint reads where the bodies before it lie, which a state left out does not
/// say until it is evaluated.
#[test]
fn thinned_models_open_as_evaluated() {
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/components-arm.solidcraft");
for sample in ["Sample Plate", "Plastic Enclosure", "components-arm"] {
let built = match solidcraft_engine::sample::samples().into_iter().find(|s| s.name == sample) {
Some(s) => {
let mut built = Session::default();
for c in (s.script)()["commands"].as_array().unwrap() {
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
built
}
None => open(&corpus),
};
let saved = built.model.saved(&built.design()).unwrap();
// Every body a megabyte (the records fit in the room left over): each budget keeps one more
// body's worth of states.
let body = 1 << 20;
let pieces: Vec<Vec<(usize, usize)>> = (0..saved.bodies.len()).map(|i| vec![(i, body)]).collect();
let thinned: Vec<SavedModel> =
(0..=saved.bodies.len()).filter_map(|n| saved.thinned(&pieces, n * body + body - 1).map(|(m, _, _)| m)).collect();
let kept = |m: &SavedModel| m.records["features"].as_array().unwrap().iter().filter(|f| f.get("state").is_some()).count();
let smallest = thinned.first().unwrap();
assert!(smallest.records["states"].as_array().unwrap().len() < saved.records["states"].as_array().unwrap().len(), "{sample}");
let more = thinned.iter().find(|m| kept(m) > kept(smallest) && kept(m) < built.doc.features.len()).unwrap();
check_thinned(&format!("{sample} smallest"), &built, smallest);
check_thinned(&format!("{sample} thinned"), &built, more);
}
}
/// Opened from its model, a design evaluates only from the first feature an edit changes: the
/// saved states before it are kept as they were read (the same states, not evaluated again).
#[test]
+1 -1
View File
@@ -30,5 +30,5 @@ 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",
"BeforeUnloadEvent", "Event", "EventTarget", "Response", "MediaQueryList", "Gamepad", "Performance",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
] }
+3 -1
View File
@@ -499,7 +499,9 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
input.set_type("file");
input.set_accept(".solidcraft,.solvecraft,.json,.step,.stp,.3mf,.stl,.obj,.glb,.gltf");
// Everything the browser build imports, from the format registry (no cadmpeg formats here).
let accept = solidcraft_engine::io::formats::open_extensions(false).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
input.set_accept(&accept);
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
let _ = input.set_attribute("style", "display:none");
if let Some(body) = doc.body() {
+43
View File
@@ -17,6 +17,14 @@ mod install;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
/// Ctrl+PageDown already reach the browser.)
#[cfg(any(target_arch = "wasm32", test))]
fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
ctrl && key == "Tab"
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
@@ -84,6 +92,20 @@ mod web {
on_unload.forget();
}
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
/// the canvas, before eframe sees them; the browser then acts on them as usual.
fn leave_tab_switching_to_browser(window: &web_sys::Window) {
let on_key = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(|e: web_sys::KeyboardEvent| {
if crate::browser_switches_tabs(&e.key(), e.ctrl_key()) {
e.stop_propagation();
}
});
let _ = window.add_event_listener_with_callback_and_bool("keydown", on_key.as_ref().unchecked_ref(), true);
// The listener lives as long as the page.
on_key.forget();
}
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
@@ -109,6 +131,7 @@ mod web {
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
font_language: String,
space_mouse: SpaceMouse,
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
}
impl Shell {
@@ -487,6 +510,10 @@ mod web {
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
}
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
let now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time));
if let Some(delay) = self.wheel_gestures.update(raw, now) {
ctx.request_repaint_after(delay);
}
files::take_drops(&self.files, raw);
// 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);
@@ -553,6 +580,7 @@ mod web {
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
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 {
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
@@ -650,6 +678,7 @@ mod web {
cjk_bytes: None,
font_language,
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
}))
}),
)
@@ -675,3 +704,17 @@ fn main() {
fn main() {
eprintln!("solidcraft-web only runs in the browser: build it with `trunk build --release` in apps/solidcraft-web");
}
#[cfg(test)]
mod tests {
use super::*;
#[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.
assert!(browser_switches_tabs("Tab", true));
// Tab and Shift+Tab move between the app's fields; Ctrl with other keys stays the app's.
assert!(!browser_switches_tabs("Tab", false));
assert!(!browser_switches_tabs("s", true));
}
}
+6 -1
View File
@@ -50,7 +50,11 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
raw-window-handle = "0.6"
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
windows-sys = { version = "0.61", features = [
"Win32_Devices_DeviceAndDriverInstallation",
"Win32_Devices_HumanInterfaceDevice",
"Win32_Devices_Properties",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Security",
@@ -66,6 +70,7 @@ windows-sys = { version = "0.61", features = [
] }
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21.2", default-features = false }
# The 3D mouse through the 3Dconnexion driver, which keeps the device to itself (src/space_mouse.rs).
solidcraft-3dx-client = { workspace = true }
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
@@ -74,7 +79,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["macos-share
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
+91
View File
@@ -0,0 +1,91 @@
//! The graphics device was lost after the window opened: the driver was reset (on Windows a TDR,
//! "Display driver stopped responding and has recovered") or the GPU went away (#4172). A lost
//! device can't draw again, so the window can't show anything more, not even a message. The
//! painter stops drawing (`egui_wgpu::DeviceLost`); the app then closes, keeping unsaved changes
//! for recovery as a forced quit does, and starts again ([`restart`]), and the new start says what
//! happened ([`note`]). A device lost again soon after such a restart starts the next one on the
//! software renderer, which doesn't depend on the graphics driver.
use crate::startup;
/// Set on the start after a lost device: when the device was lost (seconds since 1970).
pub const LOST_ENV: &str = "SOLIDCRAFT_DEVICE_LOST";
/// A loss this recent (seconds) counts for the start after it: its note, and a second loss
/// within it starts on the software renderer. Inherited by later starts, an older stamp is
/// ignored.
const RECENT_SECS: u64 = 600;
/// Seconds since 1970.
pub fn now_secs() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
}
/// Whether `stamp` ([`LOST_ENV`]) names a loss at most [`RECENT_SECS`] before `now`.
pub fn recent(stamp: Option<&str>, now: u64) -> bool {
stamp.and_then(|s| s.trim().parse::<u64>().ok()).is_some_and(|at| at <= now && now - at <= RECENT_SECS)
}
/// Whether the start after a lost device uses the software renderer: when this launch already
/// followed a recent loss (the driver keeps losing it), or ran on it anyway.
pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool {
after_recent_loss || cpu_renderer
}
/// What the start after a lost device tells the user, `software` when it runs on the software
/// renderer.
pub fn note(software: bool) -> String {
let mut note = String::from(
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
);
if software {
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
}
note
}
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
/// renderer when `software`.
pub fn restart(software: bool, lost_at: u64) {
let Ok(exe) = std::env::current_exe() else { return };
let mut command = std::process::Command::new(exe);
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
if software {
command.env(startup::RENDERER_ENV, "cpu");
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
// Not tied to this process: its own process group, no console.
command.creation_flags(0x0000_0200 | 0x0000_0008);
}
if let Err(e) = command.spawn() {
log::error!("graphics: starting again after the device was lost: {e}");
}
}
#[cfg(test)]
mod tests {
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
#[test]
fn only_a_recent_loss_counts() {
let now = 1_000_000;
assert!(super::recent(Some("999990"), now));
assert!(super::recent(Some(" 999400 "), now));
assert!(!super::recent(Some("999399"), now), "older than ten minutes");
assert!(!super::recent(Some("1000001"), now), "from the future");
assert!(!super::recent(Some("soon"), now));
assert!(!super::recent(None, now));
}
/// The first loss starts again on the GPU; a second one soon after, on the software renderer.
#[test]
fn a_second_loss_in_a_row_starts_on_the_software_renderer() {
assert!(!super::restart_on_software(false, false));
assert!(super::restart_on_software(true, false));
assert!(super::restart_on_software(false, true));
assert!(super::note(true).contains("software rendering"));
assert!(!super::note(false).contains("software rendering"));
assert!(super::note(false).contains("kept for recovery"));
}
}
+126 -48
View File
@@ -19,11 +19,14 @@
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
mod control_server;
mod device_lost;
#[cfg(target_os = "windows")]
mod native_crash;
#[cfg(target_os = "windows")]
mod native_frame;
#[cfg(target_os = "macos")]
mod native_menu;
#[cfg(target_os = "macos")]
mod open_documents;
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
mod space_mouse;
@@ -76,8 +79,12 @@ struct App {
frames: u32,
/// Started over the control channel: the window doesn't ask for the foreground.
take_focus_unwanted: bool,
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
gpu: Option<LostWatch>,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
space_mouse: space_mouse::SpaceMouse,
#[cfg(target_os = "macos")]
native_menu: Option<native_menu::NativeMenu>,
}
impl eframe::App for App {
@@ -107,6 +114,7 @@ impl eframe::App for App {
}
}
}
self.watch_device();
// The design on screen is the one whose commands a crash report replays.
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
@@ -119,6 +127,10 @@ impl eframe::App for App {
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
self.solve.open_path_async(&f);
}
#[cfg(target_os = "macos")]
if let Some(menu) = &mut self.native_menu {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
@@ -129,6 +141,10 @@ impl eframe::App for App {
}
}
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
#[cfg(target_os = "macos")]
if let Some(menu) = &mut self.native_menu {
menu.raw_input_hook(&mut self.solve, _ctx, raw);
}
// Before the app's own hook, which adds the control channel's events.
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
@@ -147,6 +163,9 @@ impl eframe::App for App {
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_engine::crash::end_session();
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
}
// An update asked to restart into the new version or to run its installer.
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
let mut command = std::process::Command::new(&program);
@@ -166,6 +185,48 @@ impl eframe::App for App {
}
}
/// The window's graphics device, watched for its loss.
struct LostWatch {
lost: eframe::egui_wgpu::DeviceLost,
/// Where the next start runs once this device is lost ([`device_lost::restart_on_software`]).
restart_on_software: bool,
/// When the loss was seen: the app is closing to start again.
lost_at: Option<u64>,
}
impl App {
/// The graphics device was lost (#4172): the window can't draw again, so close, keeping
/// unsaved changes for recovery as a forced quit does (the window can't ask about them), and
/// start again once closed ([`device_lost::restart`]).
fn watch_device(&mut self) {
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
let reason = gpu.lost.reason().unwrap_or_default();
log::error!("graphics: the device was lost ({reason}); closing to start again");
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
gpu.lost_at = Some(device_lost::now_secs());
self.solve.quit_requested = true;
}
}
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
/// case, plus FreeCAD's own mixed casing.
fn case_variants(exts: &[&str]) -> Vec<String> {
let mut all: Vec<String> = Vec::new();
for e in exts {
let mut forms = vec![e.to_ascii_lowercase(), e.to_ascii_uppercase()];
if e.eq_ignore_ascii_case("fcstd") {
forms.push("FCStd".into());
}
for v in forms {
if !all.contains(&v) {
all.push(v);
}
}
}
all
}
fn services(cc: &eframe::CreationContext<'_>) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
@@ -177,48 +238,14 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
let mut dialog = open_parent.clone();
if !first.is_empty() {
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
let both: Vec<String> = first.iter().flat_map(|e| [e.to_lowercase(), e.to_uppercase()]).collect();
dialog = dialog.add_filter(label, &both);
dialog = dialog.add_filter(label, &case_variants(first));
}
let future = dialog
.add_filter(
"Designs, CAD files and meshes",
&[
io::DESIGN_EXT,
io::LEGACY_DESIGN_EXT,
"json",
"step",
"stp",
"STEP",
"STP",
"igs",
"iges",
"IGS",
"IGES",
"FCStd",
"fcstd",
"3dm",
"3DM",
"3mf",
"3MF",
"stl",
"STL",
"obj",
"OBJ",
"glb",
"GLB",
"gltf",
"GLTF",
],
)
.add_filter("SolidCraft design (container or JSON)", &[io::DESIGN_EXT, io::LEGACY_DESIGN_EXT, "json"])
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
.add_filter("FreeCAD and Rhino", &["FCStd", "fcstd", "3dm", "3DM"])
.add_filter("Mesh (3MF, STL, OBJ, glTF)", &["3mf", "3MF", "stl", "STL", "obj", "OBJ", "glb", "GLB", "gltf", "GLTF"])
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
.add_filter("All files", &["*"])
.pick_file();
// The groups come from the format registry, so a new format joins this dialog by itself.
dialog = dialog.add_filter("Designs, CAD files and meshes", &case_variants(&io::formats::open_extensions(true)));
for (label, exts) in io::formats::import_filters(true) {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
let future = dialog.add_filter("All files", &["*"]).pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
// One kind of file (the agent chat's attachments): only the types it names, as given and in
@@ -483,6 +510,18 @@ fn backend_preference() -> Option<String> {
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
}
/// The viewport scene's sample count: the app's anti-aliasing (the window's pass is 1x), clamped
/// by the backend's policy (OpenGL renders 1x, see `effective_msaa_samples`). A count the device
/// cannot render with falls back to the supported default.
fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]) -> u32 {
let requested = eframe::egui_wgpu::effective_msaa_samples(backend, samples.max(1));
if requested <= 1 || supported.is_empty() || supported.contains(&requested) {
requested
} else {
supported.iter().copied().filter(|&n| n <= solidcraft_ui_egui::prefs::Prefs::default().msaa).max().unwrap_or(1)
}
}
/// Anti-aliasing samples from Preferences (4 when unset).
fn msaa() -> u16 {
graphics()
@@ -529,6 +568,8 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
const APP_ID: &str = solidcraft_engine::io::APP_ID;
fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
mark("main");
adopt_legacy_settings();
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
@@ -579,6 +620,11 @@ fn main() -> eframe::Result {
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
}
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
// The launch before this one closed because its graphics device was lost (`App::watch_device`).
let after_loss = device_lost::recent(std::env::var(device_lost::LOST_ENV).ok().as_deref(), device_lost::now_secs());
if after_loss {
notes.push(device_lost::note(cpu_renderer));
}
if safe_mode {
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
}
@@ -604,9 +650,11 @@ fn main() -> eframe::Result {
solidcraft_engine::crash::phase("window");
let options = eframe::NativeOptions {
viewport: app_viewport()?,
depth_buffer: depth_bits,
stencil_buffer: STENCIL_BITS,
multisampling: samples,
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
depth_buffer: 0,
stencil_buffer: 0,
multisampling: 1,
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
// and writes a scratch copy, leaving the user's settings as they were.
persistence_path: if safe_mode {
@@ -669,10 +717,16 @@ fn main() -> eframe::Result {
app.load_prefs(&p);
}
mark("app state and prefs");
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
lost: rs.device_lost.clone(),
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
lost_at: None,
});
if let Some(rs) = &cc.wgpu_render_state {
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
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)");
app.set_wgpu(rs, depth_bits, STENCIL_BITS, rs.msaa_samples);
app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
// The graphics driver is loaded now; it may have put its own exception filter
// in front of ours.
@@ -682,8 +736,8 @@ fn main() -> eframe::Result {
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
let info = rs.adapter.get_info();
solidcraft_engine::crash::set_environment(&format!(
"Renderer: {:?} {} ({}), MSAA {}x",
info.backend, info.name, info.driver, rs.msaa_samples
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
info.backend, info.name, info.driver
));
}
mark("viewport pipelines");
@@ -739,6 +793,18 @@ fn main() -> eframe::Result {
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
#[cfg(target_os = "macos")]
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
Ok(menu) => Some(menu),
Err(error) => {
app.set_status(format!("System menu: {error}"), true);
None
}
};
#[cfg(target_os = "macos")]
{
app.native_file_menu = native_menu.is_some();
}
mark("app created");
solidcraft_engine::crash::phase("first frame");
Ok(Box::new(App {
@@ -746,8 +812,11 @@ fn main() -> eframe::Result {
starting,
frames: 0,
take_focus_unwanted: control_port.is_some(),
gpu,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
space_mouse,
#[cfg(target_os = "macos")]
native_menu,
}))
}),
);
@@ -909,4 +978,13 @@ mod msaa_tests {
}
}
}
#[test]
fn the_scene_follows_the_backend_policy_too() {
use eframe::wgpu::Backend;
assert_eq!(super::scene_samples(Backend::Gl, 4, &[1, 4]), 1);
assert_eq!(super::scene_samples(Backend::Gl, 4, &[]), 1);
assert_eq!(super::scene_samples(Backend::Vulkan, 4, &[1, 4]), 4);
assert_eq!(super::scene_samples(Backend::Dx12, 8, &[1, 2, 4]), 4);
}
}
+189
View File
@@ -0,0 +1,189 @@
//! macOS system menus, owned and updated on eframe's main thread.
use std::sync::mpsc::{Receiver, sync_channel};
use muda::{
AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
accelerator::{Accelerator, Code, Modifiers},
};
use solidcraft_ui_egui::{SolveApp, toolbar::FileAction};
pub struct NativeMenu {
menu: Menu,
actions: Vec<(MenuItem, FileAction)>,
help_actions: Vec<(MenuItem, &'static str)>,
events: Receiver<MenuEvent>,
recent: Submenu,
recent_paths: Vec<String>,
}
impl NativeMenu {
pub fn new(app: &SolveApp, ctx: &egui::Context) -> muda::Result<Self> {
let menu = Menu::new();
let application = Submenu::new("SolidCraft", true);
let file = Submenu::new(app.tr("ui.file"), true);
let edit = Submenu::new(app.tr_label("Edit"), true);
let recent = Submenu::new(app.tr("ui.open_recent"), true);
let window = Submenu::new("Window", true);
let help = Submenu::new(app.tr("ui.help"), true);
let mut help_actions = Vec::new();
let mut actions = Vec::new();
let mut add = |parent: &Submenu, label: &str, action: FileAction, shortcut: Option<Accelerator>| -> muda::Result<()> {
let item = MenuItem::new(label, Self::action_enabled(&action, app, ctx), shortcut);
parent.append(&item)?;
actions.push((item, action));
Ok(())
};
application.append(&PredefinedMenuItem::about(
Some("About SolidCraft"),
Some(AboutMetadata { name: Some("SolidCraft".into()), version: Some(env!("CARGO_PKG_VERSION").into()), ..Default::default() }),
))?;
application.append(&PredefinedMenuItem::separator())?;
add(
&application,
&format!("{}\u{2026}", app.tr("ui.preferences")),
FileAction::Settings,
Some(Accelerator::new(Modifiers::META, Code::Comma)),
)?;
add(&application, &format!("{}\u{2026}", app.tr("ui.keyboard_shortcuts")), FileAction::Shortcuts, None)?;
application.append(&PredefinedMenuItem::separator())?;
application.append(&PredefinedMenuItem::services(None))?;
application.append(&PredefinedMenuItem::separator())?;
application.append_items(&[
&PredefinedMenuItem::hide(Some("Hide SolidCraft")),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::separator(),
])?;
add(&application, &format!("{} SolidCraft", app.tr("ui.quit")), FileAction::Quit, Some(Accelerator::new(Modifiers::META, Code::KeyQ)))?;
add(&file, app.tr("ui.new_design"), FileAction::New, Some(Accelerator::new(Modifiers::META, Code::KeyN)))?;
add(&file, &format!("{}\u{2026}", app.tr("ui.open")), FileAction::Open, Some(Accelerator::new(Modifiers::META, Code::KeyO)))?;
file.append(&recent)?;
file.append(&PredefinedMenuItem::separator())?;
for (label, command) in
[("Insert STEP\u{2026}", "file.insert_step"), ("Insert Mesh\u{2026}", "file.insert_mesh"), ("Insert Design", "component.insert_design")]
{
add(&file, app.tr_label(label), FileAction::Command(command), None)?;
}
file.append(&PredefinedMenuItem::separator())?;
add(&file, app.tr("ui.save"), FileAction::Save, Some(Accelerator::new(Modifiers::META, Code::KeyS)))?;
add(
&file,
&format!("{}\u{2026}", app.tr("ui.save_as")),
FileAction::SaveAs,
Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyS)),
)?;
add(&file, app.tr_label("Save As JSON\u{2026}"), FileAction::SaveJson, None)?;
file.append(&PredefinedMenuItem::separator())?;
for (label, action) in [
("Export STL\u{2026}", FileAction::ExportMesh("stl")),
("Export 3MF\u{2026}", FileAction::ExportMesh("3mf")),
("Export STEP\u{2026}", FileAction::Export("step")),
("Export Current View as DXF\u{2026}", FileAction::Command("file.export_view_dxf")),
("Export OBJ\u{2026}", FileAction::Export("obj")),
("Export URDF\u{2026}", FileAction::Export("urdf")),
("Export Design as JSON\u{2026}", FileAction::Export("json")),
] {
add(&file, app.tr_label(label), action, None)?;
}
file.append(&PredefinedMenuItem::separator())?;
add(&file, app.tr_label("Design Properties"), FileAction::Command("document.metadata"), None)?;
add(&edit, app.tr("ui.undo"), FileAction::Undo, Some(Accelerator::new(Modifiers::META, Code::KeyZ)))?;
add(&edit, app.tr("ui.redo"), FileAction::Redo, Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyZ)))?;
add(&window, app.tr_label("Agent Chat"), FileAction::AgentChat, None)?;
window.append(&PredefinedMenuItem::separator())?;
window.append_items(&[
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::bring_all_to_front(None),
])?;
for (action, label) in [
("docs", "Documentation"),
("shortcuts", "Keyboard Shortcuts"),
("messages", "Messages\u{2026}"),
("report", "Report an Issue\u{2026}"),
("crashes", "Crash Reports\u{2026}"),
] {
let item = MenuItem::new(app.tr_label(label), true, None);
help.append(&item)?;
help_actions.push((item, action));
}
menu.append_items(&[&application, &file, &edit, &window, &help])?;
let (sender, events) = sync_channel(64);
let ctx = ctx.clone();
MenuEvent::set_event_handler(Some(move |event| {
let _ = sender.try_send(event);
ctx.request_repaint();
}));
menu.init_for_nsapp();
let mut native = Self { menu, actions, help_actions, events, recent, recent_paths: Vec::new() };
native.update_recent(app)?;
Ok(native)
}
pub fn raw_input_hook(&mut self, app: &mut SolveApp, ctx: &egui::Context, raw: &mut egui::RawInput) {
for event in self.events.try_iter().take(64) {
if let Some((_, action)) = self.help_actions.iter().find(|(item, _)| item.id() == &event.id) {
solidcraft_ui_egui::help::run(app, ctx, action);
}
if let Some((_, action)) = self.actions.iter().find(|(item, _)| item.id() == &event.id) {
if matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() {
raw.events.push(egui::Event::Key {
key: egui::Key::Z,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers { command: true, mac_cmd: true, shift: matches!(action, FileAction::Redo), ..Default::default() },
});
} else {
action.run(app);
}
}
}
}
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
action.enabled(app)
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
for (item, action) in &self.actions {
let enabled = Self::action_enabled(action, app, ctx);
if item.is_enabled() != enabled {
item.set_enabled(enabled);
}
}
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
&& let Err(error) = self.update_recent(app)
{
app.set_status(format!("Recent designs menu: {error}"), true);
}
}
fn update_recent(&mut self, app: &SolveApp) -> muda::Result<()> {
while self.recent.remove_at(0).is_some() {}
self.actions.retain(|(_, action)| !matches!(action, FileAction::OpenRecent(_) | FileAction::ClearRecent));
self.recent_paths = app.home.recent.iter().take(32).cloned().collect();
for path in &self.recent_paths {
let action = FileAction::OpenRecent(path.clone());
let item = MenuItem::new(path, action.enabled(app), None);
self.recent.append(&item)?;
self.actions.push((item, action));
}
self.recent.append(&PredefinedMenuItem::separator())?;
let clear = MenuItem::new(app.tr("ui.clear_recent"), !self.recent_paths.is_empty(), None);
self.recent.append(&clear)?;
self.actions.push((clear, FileAction::ClearRecent));
Ok(())
}
}
impl Drop for NativeMenu {
fn drop(&mut self) {
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
self.menu.remove_for_nsapp();
}
}
+81 -14
View File
@@ -7,7 +7,11 @@
mod hidraw;
#[cfg(target_os = "linux")]
mod spacenavd;
#[cfg(target_os = "windows")]
mod windows_hid;
#[cfg(any(target_os = "windows", target_os = "macos"))]
use std::ffi::CString;
#[cfg(target_os = "linux")]
use std::sync::atomic::AtomicU8;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -15,8 +19,10 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use egui::Context;
#[cfg(target_os = "macos")]
use hidapi::DeviceInfo;
#[cfg(any(target_os = "windows", target_os = "macos"))]
use hidapi::{DeviceInfo, HidApi};
use hidapi::HidApi;
use quick_xml::Reader;
use quick_xml::events::Event;
use solidcraft_space_mouse::Layout;
@@ -256,8 +262,9 @@ fn at_rest(calibration: Option<Calibration>) -> Axes {
}
/// The Generic Desktop "Multi-Axis Controller" usage names a 3D mouse's motion interface, whoever
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice.
#[cfg(any(target_os = "windows", target_os = "macos"))]
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice. (Windows asks Plug
/// and Play for the same usage: `windows_hid`.)
#[cfg(target_os = "macos")]
fn is_3d_mouse(info: &DeviceInfo) -> bool {
info.usage_page() == 0x01 && info.usage() == 0x08
}
@@ -277,15 +284,18 @@ fn decode_report(report: &[u8], layout: Layout, reading: &mut Reading, now: Inst
true
}
/// Publishes one input report from the device to the egui thread.
/// Publishes one input report from the device to the egui thread. A report that reads the same as
/// the reading it replaces is no news — a device may repeat its report at rest — so it asks for no
/// frame; while the cap moves, the view asks for the next frame itself.
fn publish(ctx: &Context, shared: &Mutex<Reading>, layout: Layout, report: &[u8]) {
if DRIVER_CONNECTED.load(Ordering::Relaxed) {
return;
}
if let Ok(mut reading) = shared.lock()
&& decode_report(report, layout, &mut reading, Instant::now())
{
ctx.request_repaint();
if let Ok(mut reading) = shared.lock() {
let before = reading.axes;
if decode_report(report, layout, &mut reading, Instant::now()) && reading.axes != before {
ctx.request_repaint();
}
}
}
@@ -317,18 +327,48 @@ fn reset(ctx: &Context, shared: &Mutex<Reading>) {
ctx.request_repaint();
}
/// The HID interfaces that are 3D mice, by path. Windows asks Plug and Play, which sends nothing to
/// the devices: hidapi's enumeration would ask every keyboard and mouse for its strings on each scan
/// (#2614, `windows_hid`).
#[cfg(target_os = "windows")]
fn candidates(_api: &mut HidApi) -> Vec<CString> {
windows_hid::multi_axis_controllers()
}
/// The HID interfaces that are 3D mice, by path, from hidapi's enumeration (IOKit's registry).
#[cfg(target_os = "macos")]
fn candidates(api: &mut HidApi) -> Vec<CString> {
if let Err(error) = api.refresh_devices() {
log::warn!("3D mouse: device scan failed: {error}");
return Vec::new();
}
api.device_list().filter(|info| is_3d_mouse(info)).map(|info| info.path().to_owned()).collect()
}
/// The HID context. On Windows hidapi's own device discovery is off, so making it asks no device
/// anything either (`candidates` finds the 3D mice). Turning discovery off panics only once a
/// context with discovery exists; the reader is the one place the app makes a context, once.
#[cfg(target_os = "windows")]
fn hid_api() -> hidapi::HidResult<HidApi> {
HidApi::disable_device_discovery();
HidApi::new()
}
#[cfg(target_os = "macos")]
fn hid_api() -> hidapi::HidResult<HidApi> {
HidApi::new()
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
let Ok(mut api) = HidApi::new() else {
let Ok(mut api) = hid_api() else {
log::warn!("3D mouse: HID initialization failed");
return;
};
loop {
if let Err(error) = api.refresh_devices() {
log::warn!("3D mouse: device scan failed: {error}");
} else if let Some((device, layout, ids)) = api.device_list().filter(|info| is_3d_mouse(info)).find_map(|info| {
let device = info
.open_device(&api)
if let Some((device, layout, ids)) = candidates(&mut api).into_iter().find_map(|path| {
let device = api
.open_path(&path)
.inspect_err(|error| {
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
// The driver having the device is the reason it cannot be opened, not a problem.
@@ -337,6 +377,8 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
}
})
.ok()?;
// Read when the device was opened, from this device alone.
let info = device.get_device_info().ok()?;
let layout = device_layout(&device, info.vendor_id())?;
Some((device, layout, (info.vendor_id(), info.product_id())))
}) {
@@ -847,6 +889,31 @@ mod tests {
assert_eq!(reading.current(now + STALE_AFTER, Axes::default()).translation, [0; 3]);
}
/// A device that repeats its reading at rest asks for no frame; a reading that changes does.
#[test]
fn a_repeated_report_at_rest_asks_for_no_frame() {
let ctx = Context::default();
let shared = Mutex::new(Reading::new(Instant::now()));
let layout = Layout::default();
let frame = || {
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {});
out.textures_delta.clear();
};
let asked = || {
frame();
ctx.repaint_causes().iter().any(|c| c.file.ends_with("space_mouse.rs"))
};
frame();
// Report 1 carries translation (see the split-layout test).
let report = [1, 0x9c, 0xff, 0x2c, 0x01, 0, 0];
publish(&ctx, &shared, layout, &report);
assert!(asked(), "a report that moves the cap asks for a frame");
publish(&ctx, &shared, layout, &report);
assert!(!asked(), "the same reading at rest is no news");
publish(&ctx, &shared, layout, &[1, 0x9d, 0xff, 0x2c, 0x01, 0, 0]);
assert!(asked(), "a changed reading asks for a frame");
}
#[test]
fn a_held_reading_stays_until_the_next_one_and_a_hid_reading_goes_stale() {
let now = Instant::now();
@@ -0,0 +1,172 @@
//! Windows: finding 3D mice without asking any other device anything (#2614).
//!
//! hidapi's enumeration opens every HID interface and reads its serial number, manufacturer and
//! product strings: on USB, three control requests to every keyboard, mouse and touchscreen. The
//! reader repeated it every two seconds while no 3D mouse was plugged in, and on some machines the
//! keyboard then kept dropping off the bus and the cursor jumped. Plug and Play already names each
//! interface's top-level usage in its hardware IDs (`HID_DEVICE_UP:0001_U:0008`: Generic Desktop,
//! Multi-Axis Controller), so the reader asks it, which sends nothing to any device, and opens only
//! the interfaces that are 3D mice.
//!
//! The configuration manager has no safe API, hence the `unsafe_code` allowance on this module
//! (the app crate denies it; see `native_crash.rs`).
#![allow(unsafe_code)]
use std::ffi::CString;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
CM_GET_DEVICE_INTERFACE_LIST_PRESENT, CM_Get_DevNode_PropertyW, CM_Get_Device_Interface_List_SizeW, CM_Get_Device_Interface_ListW,
CM_Get_Device_Interface_PropertyW, CM_LOCATE_DEVNODE_NORMAL, CM_Locate_DevNodeW, CONFIGRET, CR_BUFFER_SMALL, CR_SUCCESS,
};
use windows_sys::Win32::Devices::HumanInterfaceDevice::GUID_DEVINTERFACE_HID;
use windows_sys::Win32::Devices::Properties::{
DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING, DEVPROP_TYPE_STRING_LIST, DEVPROPTYPE,
};
use windows_sys::Win32::Foundation::DEVPROPKEY;
/// The hardware ID Windows gives a top-level collection with the Generic Desktop page's
/// Multi-Axis Controller usage.
const MULTI_AXIS_CONTROLLER: &str = "HID_DEVICE_UP:0001_U:0008";
/// Largest property read, in bytes: hardware IDs are a few hundred.
const MAX_PROPERTY_BYTES: u32 = 64 * 1024;
/// Tries at reading the interface list, which can grow between asking its size and reading it.
const LIST_TRIES: usize = 4;
/// The paths of the present HID interfaces whose top-level collection is a multi-axis controller.
pub fn multi_axis_controllers() -> Vec<CString> {
interfaces()
.into_iter()
.filter(|path| {
let Some(instance) = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()) else {
return false;
};
devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids))
})
.filter_map(|path| CString::new(path).ok())
.collect()
}
/// Whether a device with these hardware IDs is a multi-axis controller.
fn is_multi_axis(hardware_ids: &[String]) -> bool {
hardware_ids.iter().any(|id| id.eq_ignore_ascii_case(MULTI_AXIS_CONTROLLER))
}
/// The strings of a nul-separated list (`REG_MULTI_SZ`, or one string), without empty ones.
fn split_multi_sz(buffer: &[u16]) -> Vec<String> {
buffer.split(|c| *c == 0).filter(|s| !s.is_empty()).map(String::from_utf16_lossy).collect()
}
/// `s` as a nul-terminated wide string.
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
/// The paths of the present HID interfaces.
fn interfaces() -> Vec<String> {
for _ in 0..LIST_TRIES {
let mut len = 0u32;
// SAFETY: `len` is a valid place for the size; the class GUID is a static; no device filter.
let sized = unsafe { CM_Get_Device_Interface_List_SizeW(&mut len, &GUID_DEVINTERFACE_HID, null(), CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
if sized != CR_SUCCESS || len == 0 {
return Vec::new();
}
let mut buffer = vec![0u16; len as usize];
// SAFETY: `buffer` holds `len` wide characters, the length the call is given.
let listed =
unsafe { CM_Get_Device_Interface_ListW(&GUID_DEVINTERFACE_HID, null(), buffer.as_mut_ptr(), len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
match listed {
CR_SUCCESS => return split_multi_sz(&buffer),
CR_BUFFER_SMALL => continue,
_ => return Vec::new(),
}
}
Vec::new()
}
/// A string or string-list property, read by `get` (property type, buffer, its size in bytes) in
/// two calls: the size, then the value.
fn property_strings(get: impl Fn(*mut DEVPROPTYPE, *mut u8, *mut u32) -> CONFIGRET) -> Option<Vec<String>> {
let mut kind: DEVPROPTYPE = 0;
let mut size = 0u32;
if get(&mut kind, null_mut(), &mut size) != CR_BUFFER_SMALL || size == 0 || size > MAX_PROPERTY_BYTES {
return None;
}
let mut buffer = vec![0u16; (size as usize).div_ceil(2)];
let mut size = (buffer.len() * 2) as u32;
if get(&mut kind, buffer.as_mut_ptr().cast(), &mut size) != CR_SUCCESS || (kind != DEVPROP_TYPE_STRING && kind != DEVPROP_TYPE_STRING_LIST) {
return None;
}
Some(split_multi_sz(&buffer))
}
/// A string property of the device interface at `path`.
fn interface_strings(path: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
let path = wide(path);
property_strings(|kind, buffer, size| {
// SAFETY: `path` is nul-terminated and outlives the call; `buffer` is null (asking the size)
// or holds `*size` bytes, as `property_strings` sets up.
unsafe { CM_Get_Device_Interface_PropertyW(path.as_ptr(), key, kind, buffer, size, 0) }
})
}
/// A string property of the device with instance ID `instance`.
fn devnode_strings(instance: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
let instance = wide(instance);
let mut devnode = 0u32;
// SAFETY: `instance` is nul-terminated and outlives the call; `devnode` is a valid place for the result.
if unsafe { CM_Locate_DevNodeW(&mut devnode, instance.as_ptr(), CM_LOCATE_DEVNODE_NORMAL) } != CR_SUCCESS {
return None;
}
property_strings(|kind, buffer, size| {
// SAFETY: `buffer` is null (asking the size) or holds `*size` bytes, as `property_strings` sets up.
unsafe { CM_Get_DevNode_PropertyW(devnode, key, kind, buffer, size, 0) }
})
}
#[cfg(test)]
mod tests {
use super::*;
/// The hardware IDs Windows gives a wired 3Dconnexion device's motion interface, and a
/// keyboard's: only the first is a multi-axis controller.
#[test]
fn only_the_multi_axis_usage_is_a_3d_mouse() {
let ids = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let space_mouse = ids(&["HID\\VID_256F&PID_C635&REV_0437&MI_00", "HID\\VID_256F&UP:0001_U:0008", "HID_DEVICE_UP:0001_U:0008", "HID_DEVICE"]);
let keyboard = ids(&[
"HID\\VID_046D&PID_C31C&REV_6400&MI_00",
"HID\\VID_046D&UP:0001_U:0006",
"HID_DEVICE_SYSTEM_KEYBOARD",
"HID_DEVICE_UP:0001_U:0006",
"HID_DEVICE",
]);
let vendor = ids(&["HID\\VID_256F&UP:FF00_U:0001", "HID_DEVICE_UP:FF00_U:0001", "HID_DEVICE_UPR:FF00-FFFF", "HID_DEVICE"]);
assert!(is_multi_axis(&space_mouse));
assert!(is_multi_axis(&ids(&["hid_device_up:0001_u:0008"])), "IDs compare without regard to case");
assert!(!is_multi_axis(&keyboard) && !is_multi_axis(&vendor) && !is_multi_axis(&[]));
}
#[test]
fn nul_separated_lists_split_into_their_strings() {
let buffer: Vec<u16> = "a\\b\0HID_DEVICE\0\0".encode_utf16().collect();
assert_eq!(split_multi_sz(&buffer), ["a\\b", "HID_DEVICE"]);
assert_eq!(split_multi_sz(&wide("one")), ["one"]);
assert!(split_multi_sz(&[0, 0]).is_empty() && split_multi_sz(&[]).is_empty());
}
/// The scan asks Plug and Play only: on any machine it returns, and every path it returns is a
/// present HID interface whose hardware IDs name the multi-axis usage.
#[test]
fn the_scan_returns_only_multi_axis_interfaces() {
let found = multi_axis_controllers();
let all = interfaces();
for path in &found {
let path = path.to_str().unwrap_or_default();
assert!(all.iter().any(|p| p == path), "{path}");
let instance = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()).unwrap_or_default();
assert!(devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids)), "{path}");
}
eprintln!("{} HID interfaces present, {} multi-axis controllers", all.len(), found.len());
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+87 -66
View File
@@ -1,88 +1,93 @@
//! Original SolidCraft mark, shared by desktop, packaging and CLI.
//! Original SolidCraft mark, shared by desktop, packaging and CLI: the borderless mark, five
//! faces with transparent gaps between them, drawn from the macOS icon's
//! `macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`.
type P = (f32, f32);
/// One face in that SVG's units: its outline, and its linear gradient from one point and
/// colour to another.
struct Face {
outline: &'static [P],
from: (P, [u8; 3]),
to: (P, [u8; 3]),
}
const FACES: [Face; 5] = [
// Inner side of the upper step.
Face {
outline: &[(547.883, 282.66), (709.797, 188.211), (709.797, 375.985)],
from: ((619.14, 179.734), [29, 99, 198]),
to: ((694.687, 396.761), [21, 73, 158]),
},
// Top of the lower step.
Face {
outline: &[(727.797, 593.626), (727.797, 407.136), (889.571, 500.381)],
from: ((619.14, 413.244), [69, 159, 255]),
to: ((790.839, 605.547), [35, 121, 231]),
},
// Right face, with the recess.
Face {
outline: &[(530.0, 521.17), (691.797, 427.912), (691.797, 655.928), (907.594, 531.545), (907.594, 740.159), (530.0, 957.8)],
from: ((632.876, 399.508), [27, 102, 207]),
to: ((783.971, 968.175), [18, 69, 150]),
},
// Left face, stepped.
Face {
outline: &[(116.406, 510.769), (116.406, 303.529), (494.0, 521.17), (494.0, 957.8), (314.203, 854.167), (314.203, 624.777)],
from: ((207.063, 303.357), [37, 141, 245]),
to: ((426.837, 968.175), [20, 97, 218]),
},
// Top of the extruded profile.
Face {
outline: &[(134.374, 272.334), (512.0, 53.364), (691.828, 157.015), (476.117, 282.846), (673.752, 396.761), (512.0, 489.993)],
from: ((385.63, 53.364), [81, 173, 255]),
to: ((522.989, 510.769), [43, 131, 241]),
},
];
/// The mark's extent in SVG units: left, top, width, height.
const BOUNDS: (f32, f32, f32, f32) = (116.406, 53.364, 791.188, 904.436);
/// This block is the icon image data
pub fn rgba(size: usize) -> Result<Vec<u8>, String> {
if !(1..=2048).contains(&size) {
return Err("icon size must be 1..=2048".into());
}
type P = (f32, f32);
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
let ss = if size > 512 { 2 } else { 4 };
let n = size * ss;
let mut buf = vec![[0u8; 4]; n * n];
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
// Fill a convex polygon given in SVG units.
let mut fill = |pts: &[P], col: [u8; 3]| {
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
let (bx, by, bw, bh) = BOUNDS;
let s = n as f32 / bh; // SVG units -> samples: the mark fills the height
let (ox, oy) = ((n as f32 - bw * s) / 2.0 - bx * s, -by * s); // and is centred sideways
// Fill each face's outline (even-odd spans per row, so concave outlines work) with its gradient.
for face in &FACES {
let v: Vec<P> = face.outline.iter().map(|p| (ox + p.0 * s, oy + p.1 * s)).collect();
let (((fx, fy), c0), ((tx, ty), c1)) = (face.from, face.to);
let (gx, gy) = (tx - fx, ty - fy);
let g2 = gx * gx + gy * gy;
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
let mut xs = Vec::with_capacity(v.len());
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
let cy = y as f32 + 0.5;
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
xs.clear();
for i in 0..v.len() {
let (a, b) = (v[i], v[(i + 1) % v.len()]);
if (a.1 <= cy) != (b.1 <= cy) {
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
lo = lo.min(x);
hi = hi.max(x);
xs.push(a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0));
}
}
if lo > hi {
continue;
}
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
xs.sort_by(f32::total_cmp);
for &[from, to] in xs.as_chunks::<2>().0 {
for x in ((from - 0.5).ceil() as i32).max(0)..=((to - 0.5).floor() as i32).min(n as i32 - 1) {
// The sample's place along the gradient, in SVG units.
let (px, py) = ((x as f32 + 0.5 - ox) / s, (cy - oy) / s);
let t = (((px - fx) * gx + (py - fy) * gy) / g2).clamp(0.0, 1.0);
let mix = |k: usize| (f32::from(c0[k]) + (f32::from(c1[k]) - f32::from(c0[k])) * t).round() as u8;
buf[y as usize * n + x as usize] = [mix(0), mix(1), mix(2), 255];
}
}
}
};
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
let white = [255, 255, 255];
let mut stroke = |pts: &[P], closed: bool| {
let m = pts.len();
for i in 0..(if closed { m } else { m - 1 }) {
let (a, b) = (pts[i], pts[(i + 1) % m]);
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
}
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
let disc: Vec<P> = (0..16)
.map(|k| {
let t = k as f32 * std::f32::consts::TAU / 16.0;
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
})
.collect();
fill(&disc, white);
}
};
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
stroke(
&[
(178.5, 5.8),
(5.0, 105.9),
(5.0, 206.1),
(91.7, 256.2),
(91.7, 356.4),
(178.5, 406.4),
(352.0, 306.3),
(352.0, 206.1),
(265.2, 156.0),
(265.2, 55.9),
],
true,
);
}
// Average each ss x ss block down to one pixel (straight alpha = covered share).
let mut rgba = Vec::with_capacity(size * size * 4);
for y in 0..size {
@@ -123,4 +128,20 @@ mod tests {
assert!(super::rgba(0).is_err());
assert!(super::rgba(2049).is_err());
}
/// Borderless: no white edge lines, and the gaps between faces are see-through. At 256 px
/// the gap between the left and right faces (SVG x 494..530) is transparent on the row
/// through the middle of the mark, and no opaque pixel is white.
#[test]
fn shared_mark_is_borderless() {
let size = 256;
let pixels = super::rgba(size).expect("valid size");
let px = |x: usize, y: usize| pixels.as_chunks::<4>().0[y * size + x];
let s = size as f32 / 904.436;
let gap_x = ((size as f32 - 791.188 * s) / 2.0 + (512.0 - 116.406) * s) as usize;
let row = ((750.0 - 53.364) * s) as usize;
assert_eq!(px(gap_x, row)[3], 0, "the seam between the left and right faces is a gap");
assert!(px(gap_x - 8, row)[3] == 255 && px(gap_x + 8, row)[3] == 255, "faces on both sides of the gap");
assert!(pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 0 || p[0] < 200), "no white edges");
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 44 KiB

@@ -239,6 +239,7 @@ split_face false
spun false
star false
status_error false
status_recovery false
style false
suppress false
surface_blend false
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Keyboard shortcuts</title>
<path d="M13 16L51 16A5 5 0 0 1 56 21L56 43A5 5 0 0 1 51 48L13 48A5 5 0 0 1 8 43L8 21A5 5 0 0 1 13 16Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="20" cy="27" r="2.8" fill="currentColor"/>
<circle cx="32" cy="27" r="2.8" fill="currentColor"/>
<circle cx="44" cy="27" r="2.8" fill="currentColor"/>
<path d="M20 39L44 39" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 607 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Graphics display</title>
<path d="M13 10L51 10A5 5 0 0 1 56 15L56 37A5 5 0 0 1 51 42L13 42A5 5 0 0 1 8 37L8 15A5 5 0 0 1 13 10ZM32 42L32 54M24 54L40 54" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Mouse navigation</title>
<path d="M32 8A13 13 0 0 1 45 21L45 43A13 13 0 0 1 19 43L19 21A13 13 0 0 1 32 8ZM32 8L32 24" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Interface palette</title>
<path d="M32 8C18.75 8 8 18.75 8 32C8 45.25 18.75 56 32 56C37 56 39 52.5 36.5 49C34 45.5 36 42 40 42L45 42C51.1 42 56 37.1 56 31C56 19 45 8 32 8Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="21" cy="29" r="2.8" fill="currentColor"/>
<circle cx="29" cy="20" r="2.8" fill="currentColor"/>
<circle cx="41" cy="23" r="2.8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Plugins</title>
<path d="M24 8L24 21M40 8L40 21M16 21L48 21L48 29A16 16 0 0 1 16 29L16 21ZM32 45L32 56" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Recover unsaved design</title>
<circle cx="32" cy="33" r="22" fill="#b9ddf8"/>
<path d="M10 33A22 22 0 1 0 16.44 17.44L10 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L10 24L22 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 21L32 33L41 39" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

+15 -3
View File
@@ -99,14 +99,23 @@ pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
let s = 20.0 / 412.217;
brand_mark_styled(p, c, 20.0, crate::theme::Tokens::get().logo, true);
}
/// The same original mark as a single quiet silhouette, without face seams or outlines.
pub(crate) fn brand_mark_silhouette(p: &egui::Painter, c: Pos2, height: f32, ink: Color32) {
brand_mark_styled(p, c, height, [ink; 3], false);
}
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 3], edges: bool) {
let s = height / 412.217;
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
let face = |col: Color32, pts: &[(f32, f32)]| {
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
};
// The faces are separated by gaps that show the application bar, not by white lines.
let [light, mid, dark, _] = crate::theme::Tokens::get().logo;
let [light, mid, dark] = colours;
let edge_col = crate::theme::Tokens::get().app_bar;
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
@@ -117,7 +126,10 @@ pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
// White edge lines.
if !edges {
return;
}
// The seams between the faces and around the mark, in the bar's colour, so they read as gaps.
let edge = Stroke::new(10.0 * s, edge_col);
let line = |pts: &[(f32, f32)]| {
p.add(egui::Shape::line(m(pts), edge));
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Licence MIT",
"help.notice": "NOTICE",
"help.third_party": "Licence třetích stran",
"home.back": "Zpět na {name}",
"home.crashes": "Hlášení o pádech",
"home.design_workspace": "Návrh",
"home.documentation": "Dokumentace",
"home.export": "Exportovat",
"home.export_hint": "Vyberte formát pro aktuální návrh.",
"home.export_json_hint": "Upravitelný návrh a historie příkazů",
"home.export_mesh_hint": "Síť pro tisk nebo sdílení",
"home.export_step_hint": "Přesná geometrie tělesa",
"home.import": "Importovat…",
"home.import_hint": "STEP, IGES nebo síť do nového návrhu",
"home.in_this_browser": "v tomto prohlížeči",
"home.missing": "chybí",
"home.month_01": "led",
"home.month_02": "úno",
"home.month_03": "bře",
"home.month_04": "dub",
"home.month_05": "kvě",
"home.month_06": "čer",
"home.month_07": "čvc",
"home.month_08": "srp",
"home.month_09": "zář",
"home.month_10": "říj",
"home.month_11": "lis",
"home.month_12": "pro",
"home.new_inches": "Nový návrh v palcích",
"home.new_inches_hint": "Palce · otevře se v pracovním prostoru Návrh",
"home.new_mm": "Nový návrh",
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
"home.open": "Otevřít…",
"home.opened_days_ago": "Otevřeno před {count} dny",
"home.opened_hours_ago": "Otevřeno před {count} h",
"home.opened_just_now": "Otevřeno právě teď",
"home.opened_minutes_ago": "Otevřeno před {count} min",
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
"home.open_now": "Otevřít nyní",
"home.opened_days_ago": "Před {count} dny",
"home.opened_hours_ago": "Před {count} h",
"home.opened_just_now": "Právě teď",
"home.opened_minutes_ago": "Před {count} min",
"home.opening": "otevírání…",
"home.properties": "Vlastnosti návrhu",
"home.recent": "Nedávné",
"home.recent_empty": "Zde se zobrazí návrhy, které otevřete nebo uložíte.",
"home.recovery_count": "Návrhy k obnovení: {count}",
"home.recovery_empty": "Žádné návrhy k obnovení.",
"home.recovery_hint": "SolidCraft se zavřel před uložením těchto návrhů.",
"home.report_issue": "Nahlásit problém",
"home.samples": "Ukázky",
"home.settings": "Nastavení",
"home.short_date": "{day}. {month}",
"home.shortcuts": "Klávesové zkratky",
"home.start_hint": "Vytvořte návrh, otevřete nedávný soubor nebo prozkoumejte ukázku.",
"home.whats_new": "Co je nového",
"home.yesterday": "Včera",
"install.button_tip": "Nainstalovat SolidCraft jako aplikaci: otevře se ve vlastním okně a funguje i offline",
"install.dock_1": "Zvolte Soubor › Přidat do Docku.",
"install.dock_2": "Klikněte na Přidat.",
@@ -1022,7 +1060,7 @@
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
"search.valid": "Platné",
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu. Dokud je zapnuto, pohled se překresluje nepřetržitě.",
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
"settings.msaa_applies_on_restart": "Vyhlazování se projeví při příštím spuštění SolidCraftu",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "K jedné zprávě lze připojit nejvýše {max} souborů; vynecháno: {files}",
"ui.agent_chat_files_loading": "Čekání na přečtení souborů",
"ui.agent_chat_files_max": "K jedné zprávě nejvýše {max} souborů",
"ui.agent_chat_flatpak_none_found": "Nebyl nalezen žádný kódovací agent. Sandbox Flatpaku ve výchozím stavu nevidí agenty nainstalované ve vaší domovské složce a nemá přístup k síti. Chcete-li povolit Claude Code a jeho adaptér ACP nainstalované přes nvm nebo do ~/.local/bin, spusťte v terminálu tento příkaz a poté SolidCraft restartujte. Udělí přístup k uvedeným programům a přihlašovacím souborům a také přístup k síti:",
"ui.agent_chat_import_failed": "{name}: import selhal: {error}",
"ui.agent_chat_intro": "Chatujte s kódovacím agentem, kterého již používáte, nebo s modelem na zvoleném endpointu. Pracuje na otevřeném návrhu pomocí nástrojů SolidCraftu: jeho změny vidíte průběžně a každou zprávu lze vrátit jedním krokem. Co smí dělat bez ptaní, nastavíte v Předvolby > AI.",
"ui.agent_chat_key_in_env": "klíč nastaven",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Boční pohledy zobrazí mřížku na svislé rovině, která je k nim přivrácená, místo na zemi",
"ui.ground_shadow": "Stín na podložce",
"ui.groupSelected": "Seskupit vybrané",
"ui.hatch": "Šrafování",
"ui.heading_add_ins": "DOPLŇKY",
"ui.heading_assemble": "SESTAVA",
"ui.heading_box": "KVÁDR",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "MIT-licens",
"help.notice": "NOTICE",
"help.third_party": "Tredjepartslicenser",
"home.back": "Tilbage til {name}",
"home.crashes": "Nedbrudsrapporter",
"home.design_workspace": "Design",
"home.documentation": "Dokumentation",
"home.export": "Eksportér",
"home.export_hint": "Vælg et format til det aktuelle design.",
"home.export_json_hint": "Redigerbart design og kommandohistorik",
"home.export_mesh_hint": "Mesh til udskrivning eller deling",
"home.export_step_hint": "Præcis geometri for massive legemer",
"home.import": "Importér…",
"home.import_hint": "STEP, IGES eller mesh til et nyt design",
"home.in_this_browser": "i denne webbrowser",
"home.missing": "mangler",
"home.month_01": "jan.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "apr.",
"home.month_05": "maj",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "aug.",
"home.month_09": "sep.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Nyt design i tommer",
"home.new_inches_hint": "Tommer · åbner i Design",
"home.new_mm": "Nyt design",
"home.new_mm_hint": "Millimeter · åbner i Design",
"home.open": "Åbn…",
"home.opened_days_ago": "Åbnet for {count} dage siden",
"home.opened_hours_ago": "Åbnet for {count} t siden",
"home.opened_just_now": "Netop åbnet",
"home.opened_minutes_ago": "Åbnet for {count} min siden",
"home.open_hint": "Et .solidcraft-design ({shortcut})",
"home.open_now": "Åbn nu",
"home.opened_days_ago": "For {count} dage siden",
"home.opened_hours_ago": "For {count} t siden",
"home.opened_just_now": "Lige nu",
"home.opened_minutes_ago": "For {count} min siden",
"home.opening": "åbner…",
"home.properties": "Designegenskaber",
"home.recent": "Seneste",
"home.recent_empty": "Design, du åbner eller gemmer, vises her.",
"home.recovery_count": "Design til gendannelse: {count}",
"home.recovery_empty": "Ingen design kan gendannes.",
"home.recovery_hint": "SolidCraft lukkede, før disse design blev gemt.",
"home.report_issue": "Rapportér et problem",
"home.samples": "Eksempler",
"home.settings": "Indstillinger",
"home.short_date": "{day}. {month}",
"home.shortcuts": "Tastaturgenveje",
"home.start_hint": "Opret et design, åbn en nylig fil, eller udforsk et eksempel.",
"home.whats_new": "Nyheder",
"home.yesterday": "I går",
"install.button_tip": "Installér SolidCraft som en app: Den åbner i sit eget vindue og virker offline",
"install.dock_1": "Vælg Arkiv › Føj til Dock.",
"install.dock_2": "Klik på Tilføj.",
@@ -1022,7 +1060,7 @@
"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.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen. Når dette er slået til, gentegnes visningen løbende.",
"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",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "Højst {max} filer pr. besked; udeladt: {files}",
"ui.agent_chat_files_loading": "Venter på indlæsning af filerne",
"ui.agent_chat_files_max": "Højst {max} filer i én besked",
"ui.agent_chat_flatpak_none_found": "Ingen kodeagent fundet. Flatpak-sandkassen kan som standard ikke se agenter, der er installeret i din hjemmemappe, og har ingen netværksadgang. For at tillade Claude Code og dets ACP-adapter installeret via nvm eller i ~/.local/bin skal du køre denne kommando i en terminal og derefter genstarte SolidCraft. Den giver adgang til de nævnte programmer og loginfiler samt netværksadgang:",
"ui.agent_chat_import_failed": "{name}: Importen mislykkedes: {error}",
"ui.agent_chat_intro": "Chat med en kodeagent, du allerede bruger, eller en modeltjeneste, du vælger. Den arbejder på det åbne design gennem SolidCrafts værktøjer: Ændringer vises undervejs og fortrydes som ét trin pr. besked. Hvad den må gøre uden at spørge, angives i Indstillinger > AI.",
"ui.agent_chat_key_in_env": "nøgle angivet",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Sidevisninger viser gitteret på det lodrette plan, der vender mod dem, i stedet for grundplanet",
"ui.ground_shadow": "Skygge på grundplan",
"ui.groupSelected": "Gruppér valgte",
"ui.hatch": "Skravering",
"ui.heading_add_ins": "ADD-INS",
"ui.heading_assemble": "SAMLING",
"ui.heading_box": "BOKS",
+170 -5
View File
@@ -14,11 +14,15 @@
"Active": "Aktiv",
"Active rule": "Aktive Regel",
"Add": "Hinzufügen",
"Add Column": "Spalte hinzufügen",
"Add Key": "Schlüssel hinzufügen",
"Add Mid Radius": "Mittleren Radius hinzufügen",
"Add Row": "Zeile hinzufügen",
"Add a tool": "Werkzeug hinzufügen",
"Add condition": "Bedingung hinzufügen",
"Add or edit rule": "Regel hinzufügen oder bearbeiten",
"Add to New Folder": "Zu neuem Ordner hinzufügen",
"Advanced": "Erweitert",
"Against": "Gegen",
"Alignment": "Ausrichtung",
"All Sketch Points": "Alle Skizzenpunkte",
@@ -31,6 +35,7 @@
"Appearance name": "Name des Erscheinungsbilds",
"Applied Radius": "Angewendeter Radius",
"Apply To": "Anwenden auf",
"Apply library appearance": "Erscheinungsbild aus der Bibliothek anwenden",
"Apply to selection": "Auf Auswahl anwenden",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Wenden Sie zuerst Ihr Erscheinungsbild an. Exportiert zugewiesene Erscheinungsbilder und in diesem Design erstellte Materialien.",
"Arc length": "Bogenlänge",
@@ -41,13 +46,18 @@
"BLEND SURFACE": "ÜBERGANGSFLÄCHE",
"BOUNDARY SURFACE": "BERANDUNGSFLÄCHE",
"BROWSER": "BROWSER",
"Bend Edges": "Biegekanten",
"Bend Line Position": "Position der Biegelinie",
"Bend Position": "Biegeposition",
"Bend Radius": "Biegeradius",
"Bend Table": "Biegetabelle",
"Bends": "Biegungen",
"Bodies": "Körper",
"Bodies (optional)": "Körper (optional)",
"Body": "Körper",
"Body (last if none)": "Körper (letzter, wenn keiner)",
"Body to split": "Zu teilender Körper",
"Body, or faces to take out": "Körper oder zu entfernende Flächen",
"Boundary": "Begrenzung",
"Bounding box": "Hüllquader",
"CHECK": "PRÜFEN",
@@ -59,7 +69,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
"Capture All Positions": "Alle Positionen erfassen",
"Capture Pos.": "Pos. erfassen",
"Catch Depth": "Fangtiefe",
"Catch Direction": "Fangrichtung",
"Catch Length": "Fanglänge",
"Cells": "Zellen",
"Center X": "Mittelpunkt X",
"Center Y": "Mittelpunkt Y",
@@ -68,15 +80,21 @@
"Chamfer Type": "Abschrägungstyp",
"Check collisions": "Kollisionen prüfen",
"Checked": "Geprüft",
"Choose Variable radius or Spline in Type.": "Unter „Typ“ „Variable radius“ oder „Spline“ wählen.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Wählen Sie eine neue Ebene. Skizzengeometrie und Bemaßungen bleiben in ihren zweidimensionalen Koordinaten unverändert.",
"Choose…": "Auswählen…",
"Circular": "Kreisförmig",
"Circumference": "Umfang",
"Close draft": "Entwurf schließen",
"Collapse": "Einklappen",
"Collisions": "Kollisionen",
"Colour": "Farbe",
"Command": "Befehl",
"Comment": "Kommentar",
"Component 1": "Komponente 1",
"Component 2": "Komponente 2",
"Component1": "Komponente1",
"Components": "Komponenten",
"Config Table": "Konfigurationstabelle",
"Configuration": "Konfiguration",
"Configuration rules": "Configuration rules",
@@ -84,6 +102,7 @@
"Contact Sets": "Kontaktsätze",
"Contact sets": "Kontaktsätze",
"Continuity": "Stetigkeit",
"Control Radius": "Kontrollradius",
"Copy Error Message": "Fehlermeldung kopieren",
"Copy Warning Message": "Warnmeldung kopieren",
"Copy all licences": "Alle Lizenzen kopieren",
@@ -93,8 +112,11 @@
"Corner Relief": "Eckenentlastung",
"Corner Relief Shape": "Form der Eckenentlastung",
"Counterbore depth": "Tiefe der Senkung",
"Counterbore Ø": "Tiefenbohrung Ø",
"Countersink angle": "Winkel der Kegelsenkung",
"Countersink Ø": "Senkung Ø",
"Create Issue — review draft": "Issue erstellen — Entwurf prüfen",
"Create Issue…": "Issue erstellen…",
"Cross curves": "Querkurven",
"Cube": "Würfel",
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
@@ -103,7 +125,11 @@
"Curvature (G2)": "Krümmung (G2)",
"Curvature change (G3)": "Krümmungsänderung (G3)",
"Customize Toolbar": "Symbolleiste anpassen",
"Customize…": "Anpassen…",
"Cylindrical face": "Zylindrische Fläche",
"Delete": "Löschen",
"Delete Selected Features…": "Ausgewählte Merkmale löschen…",
"Delete Selected Folders": "Ausgewählte Ordner löschen",
"Delete all": "Alle löschen",
"Delete rule": "Regel löschen",
"Delete the row": "Zeile löschen",
@@ -112,8 +138,11 @@
"Dependents": "Abhängige Elemente",
"Designs you open or save appear here.": "Designs, die Sie öffnen oder speichern, erscheinen hier.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestimmt durch Bis Objekt. Wählen Sie Winkel, um den Winkelbereich zu bearbeiten.",
"Diameter": "Durchmesser",
"Direction": "Richtung",
"Discard": "Verwerfen",
"Distance": "Abstand",
"Distance 1": "Abstand 1",
"Do not export hidden edges": "Verdeckte Kanten nicht exportieren",
"Don't ask again": "Nicht mehr fragen",
"Double-click to rename": "Double-click to rename",
@@ -125,17 +154,21 @@
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
"Enable rule": "Regel aktivieren",
"Enabled": "Aktiviert",
"End Radius": "Endradius",
"End bulge": "Endausbauchung",
"End continuity": "Endstetigkeit",
"Expand": "Ausklappen",
"Explode": "Explodieren",
"Explode Text": "Text auflösen",
"Export Animation…": "Animation exportieren…",
"Export Bend Table…": "Biegetabelle exportieren…",
"Export CSV…": "CSV exportieren…",
"Export Current View as DXF": "Aktuelle Ansicht als DXF exportieren",
"Export Current View as DXF…": "Aktuelle Ansicht als DXF exportieren…",
"Export DXF": "DXF exportieren",
"Export DXF…": "DXF exportieren…",
"Export JSON…": "JSON exportieren…",
"Export design library...": "Designbibliothek exportieren...",
"Export hidden edges as dashed lines": "Verdeckte Kanten gestrichelt exportieren",
"Expression": "Ausdruck",
"Extend to meet": "Verlängern, bis sie sich treffen",
@@ -143,31 +176,40 @@
"FILES": "DATEIEN",
"FLAT": "ABWICKLUNG",
"Face": "Fläche",
"Face Appearance": "Flächenerscheinungsbild",
"Face attachment lost": "Flächenanbindung verloren",
"Faces": "Flächen",
"Faces / edges / vertices": "Flächen / Kanten / Ecken",
"Faces to split": "Zu teilende Flächen",
"Fastener": "Verbindungselement",
"Favorites": "Favoriten",
"Favourite": "Favourite",
"Feature": "Funktion",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Dateien aus vielen CAD-Programmen haben Y nach oben und öffnen sich hier daher auf dem Rücken liegend (Z nach oben).",
"Filter": "Filter",
"Filter…": "Filtern…",
"Find in Browser": "Im Browser suchen",
"Finish": "Oberfläche",
"First plane": "Erste Ebene",
"Fit and G2 corners require Constant radius.": "Passung und G2-Ecken erfordern Constant radius.",
"Fixed Face": "Feste Fläche",
"Flat Pattern": "Flachmuster",
"Flat size": "Abwicklungsgröße",
"Follow camera (Look At sketches)": "Kamera folgen (Skizzen ansehen)",
"Follow the camera (Look At sketches)": "Der Kamera folgen (Skizzen ansehen)",
"Font": "Schriftart",
"From": "Von",
"From Bodies": "Aus Körpern",
"From edges": "Ausgangskanten",
"Full Length": "Volle Länge",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-Ecken erfordern einen konstanten Radius und Exact fit. Die Konstruktion von G2-Ecken wird noch nicht unterstützt.",
"Gap": "Spalt",
"Grid": "Raster",
"HISTORY": "VERLAUF",
"Height": "Höhe",
"Hidden edges": "Verdeckte Kanten",
"History layout": "Verlaufslayout",
"Hole (optional)": "Bohrung (optional)",
"Holes": "Löcher",
"Holes (edges on them)": "Löcher (ihre Kanten)",
"Home view": "Startansicht",
@@ -178,14 +220,20 @@
"In place (surfaces)": "An Ort und Stelle (Flächen)",
"In plane": "In der Ebene",
"Inside": "Innen",
"Instant": "Sofort",
"Interface font": "Schriftart der Oberfläche",
"Interferes": "Überschneidet sich",
"Invert": "Umkehren",
"Isocurve": "Isokurve",
"Issue title": "Issue-Titel",
"Items": "Elemente",
"JOINTS": "GELENKE",
"Joint Origin1": "Gelenkursprung1",
"Keep (points on it)": "Behalten (Punkte darauf)",
"Keep Tools": "Werkzeuge behalten",
"Keep of the other surface": "Vom anderen Flächenkörper behalten",
"Key a joint's value at this step": "Gelenkwert in diesem Schritt als Schlüsselbild setzen",
"Keys": "Schlüsselbilder",
"LIBRARY": "BIBLIOTHEK",
"LINKS": "VERKNÜPFUNGEN",
"Later": "Später",
@@ -196,9 +244,14 @@
"Letter Spacing": "Zeichenabstand",
"Library": "Bibliothek",
"Library file": "Bibliotheksdatei",
"Limits": "Grenzen",
"Line Spacing": "Zeilenabstand",
"Line Type": "Linientyp",
"Listing the installed fonts…": "Installierte Schriftarten werden aufgelistet…",
"Load library": "Bibliothek laden",
"Local library": "Lokale Bibliothek",
"Loop": "Schleife",
"M5 or M5x0.5": "M5 oder M5x0.5",
"MANAGE": "VERWALTEN",
"MATERIALS": "MATERIALIEN",
"MODEL": "MODELL",
@@ -209,14 +262,20 @@
"Matched restrictions check the final values.": "Zutreffende Einschränkungen prüfen die endgültigen Werte.",
"Material": "Material",
"Max": "Max",
"Maximum": "Maximum",
"Mesh body": "Netzkörper",
"Mid Radius": "Mittlerer Radius",
"Middle double-click": "Doppelklick mit mittlerer Taste",
"Min": "Min",
"Minimum": "Minimum",
"Mirror Plane": "Spiegelebene",
"Mitre": "Gehrung",
"Model Parameters": "Modellparameter",
"Modeled": "Modelliert",
"More toolbar panels": "Weitere Symbolleistenbereiche",
"Motion": "Bewegung",
"Mouse gestures": "Mausgesten",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mausgesten funktionieren nur im SolidWorks-Mausmodus (das Rad dreht, Strg + Rad verschiebt).",
"Move / Copy": "Verschieben / Kopieren",
"Move Type": "Verschiebungstyp",
"Move down": "Nach unten",
@@ -238,8 +297,11 @@
"Not available yet": "Noch nicht verfügbar",
"Notch": "Ausklinkung",
"OUTPUT": "AUSGABE",
"Of": "Freiheitsgrad",
"On your computer": "Auf diesem Computer",
"Opacity": "Deckkraft",
"Open GitHub draft": "GitHub-Entwurf öffnen",
"Open library...": "Bibliothek öffnen...",
"Open local libraries in the desktop app. You can export this design's library here.": "Öffnen Sie lokale Bibliotheken in der Desktop-App. Die Bibliothek dieses Designs können Sie hier exportieren.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Quelloffenes parametrisches 3D-CAD in Rust. Lizenziert unter MIT OR Apache-2.0.",
"Operator": "Operator",
@@ -258,8 +320,10 @@
"Pick the tool bodies": "Werkzeugkörper wählen",
"Picked": "Ausgewählte",
"Picked point": "Gewählter Punkt",
"Pivot": "Drehpunkt",
"Place at the origin instead": "Stattdessen im Ursprung platzieren",
"Plastic Rules": "Kunststoffregeln",
"Play the joint's motion in the view": "Die Bewegung des Gelenks in der Ansicht abspielen",
"Playback": "Wiedergabe",
"Plot": "Diagramm",
"Position": "Position",
@@ -267,14 +331,18 @@
"Position (G0)": "Position (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Position entlang der Kante, 0 am Anfang, 1 am anderen Ende",
"Preferences › Design turns the offer back on": "Unter Einstellungen › Design lässt sich das Angebot wieder einschalten",
"Preview": "Vorschau",
"Preview Row": "Vorschauzeile",
"Preview leaves the current design unchanged.": "Die Vorschau lässt das aktuelle Design unverändert.",
"Profile / Edges": "Profil / Kanten",
"Profiles or curves": "Profile oder Kurven",
"Profiles/Curves": "Profile/Kurven",
"Quantity": "Größe",
"RECENT": "ZULETZT",
"REFERENCE": "REFERENZ",
"RULED SURFACE": "REGELFLÄCHE",
"RULES": "REGELN",
"Radius": "Radius",
"Radius Fit": "Radiusanpassung",
"Ratio": "Verhältnis",
"Reattach selected face": "Ausgewählte Fläche neu anbinden",
@@ -283,6 +351,8 @@
"Rectangular": "Rechteckig",
"Reference face": "Referenzfläche",
"Remove Tools": "Werkzeuge entfernen",
"Remove condition": "Bedingung entfernen",
"Remove from Folder": "Aus Ordner entfernen",
"Remove from list": "Aus Liste entfernen",
"Remove override": "Überschreibung entfernen",
"Remove private design details or paths before sharing.": "Entfernen Sie vor dem Teilen private Designdetails oder Pfade.",
@@ -290,12 +360,16 @@
"Remove the column": "Spalte entfernen",
"Remove the track": "Spur entfernen",
"Remove this point": "Diesen Punkt entfernen",
"Rename Feature": "Merkmal umbenennen",
"Rename Folder": "Ordner umbenennen",
"Replace existing library file": "Vorhandene Bibliotheksdatei ersetzen",
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
"Reset Toolbar": "Symbolleiste zurücksetzen",
"Resize Part": "Teil skalieren",
"Restore View": "Ansicht wiederherstellen",
"Restore outer boundary": "Äußere Begrenzung wiederherstellen",
"Restore the inherited component or physical material appearance": "Das geerbte Erscheinungsbild der Komponente oder des physikalischen Materials wiederherstellen",
"Restore this studio's tools": "Werkzeuge dieses Studios wiederherstellen",
"Result": "Ergebnis",
"Retake screenshot": "Screenshot erneut aufnehmen",
"Reverse": "Umkehren",
@@ -305,7 +379,13 @@
"Revert Pos.": "Pos. zurücksetzen",
"Revert Position": "Position zurücksetzen",
"Review the report and screenshot before opening GitHub.": "Prüfen Sie Bericht und Screenshot, bevor Sie GitHub öffnen.",
"Rib Length": "Rippenlänge",
"Rib Offset": "Rippenversatz",
"Rib Thickness": "Rippendicke",
"Ribs": "Rippen",
"Rim Edge": "Randkante",
"Ring of tools on the right button": "Werkzeugring auf der rechten Maustaste",
"Roll Back Here": "Hierher zurücksetzen",
"Roll History Marker Here": "Verlaufsmarkierung hierher verschieben",
"Round": "Rund",
"Rule": "Rule",
@@ -322,13 +402,18 @@
"Search": "Suchen",
"Search commands": "Befehle suchen",
"Search this library": "Diese Bibliothek durchsuchen",
"Second plane": "Zweite Ebene",
"Section": "Schnitt",
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
"Select Other": "Andere auswählen",
"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).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
"Select bodies or faces instead": "Stattdessen Körper oder Flächen auswählen",
"Select edges": "Kanten auswählen",
"Selection filter": "Auswahlfilter",
"Semantic faces share their appearance across instances.": "Semantische Flächen teilen ihr Erscheinungsbild über alle Instanzen hinweg.",
"Send your report on GitHub": "Bericht auf GitHub senden",
"Shaded": "Schattiert",
"Shape": "Form",
"Sheet Rules": "Blechregeln",
"Show Agent Cursor": "Agenten-Cursor anzeigen",
@@ -346,6 +431,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft wurde beendet, bevor diese Designs gespeichert wurden. Ihr letzter Autosave kann geöffnet werden.",
"Specialise": "Spezialisieren",
"Speed": "Geschwindigkeit",
"Splitting tool": "Trennwerkzeug",
"Stand it up": "Aufrichten",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
"Stand this part up?": "Dieses Teil aufrichten?",
@@ -355,14 +441,20 @@
"Steps": "Schritte",
"Stitch the kept pieces": "Behaltene Teile vernähen",
"Stitch to both bodies": "Mit beiden Körpern vernähen",
"Studio": "Studio",
"Study": "Untersuchung",
"Style": "Stil",
"Suppress": "Unterdrücken",
"Suppressed": "Unterdrückt",
"Surface": "Flächenkörper",
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
"Swap Ends": "Enden vertauschen",
"TRIM": "TRIMMEN",
"Tangent (G1)": "Tangente (G1)",
"Tangent Chain": "Tangentenkette",
"Tangent chain": "Tangentialkette",
"Target": "Ziel",
"Target Body": "Zielkörper",
"Tear": "Reißen",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
@@ -375,24 +467,31 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Löschen entfernt es aus jeder Kopie des übergeordneten Elements.",
"This child is shared. Position changes apply to every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Positionsänderungen gelten für jede Kopie des übergeordneten Elements.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Diese Fläche kann keine dauerhafte Anbindung bieten. Wählen Sie eine andere Fläche oder verwenden Sie ausdrücklich eine feste Ebene.",
"To": "Bis",
"To Offset": "Bis Versatz",
"To edges": "Zielkanten",
"Tolerance": "Toleranz",
"Too close": "Zu nah",
"Tool Bodies": "Werkzeugkörper",
"Tools": "Werkzeuge",
"Tools, clockwise from the top": "Werkzeuge, im Uhrzeigersinn von oben",
"Trimming tool (plane, face or curves)": "Trimmwerkzeug (Ebene, Fläche oder Kurven)",
"Turn off section analysis before exporting the view.": "Deaktivieren Sie die Schnittanalyse, bevor Sie die Ansicht exportieren.",
"Type": "Typ",
"UNTRIM": "TRIMMEN AUFHEBEN",
"Unit": "Einheit",
"Unsuppress": "Unterdrückung aufheben",
"Updating the preview": "Vorschau wird aktualisiert",
"Use active author placement": "Aktive Autorplatzierung verwenden",
"Use fixed plane": "Feste Ebene verwenden",
"Use saved fixed plane": "Gespeicherte feste Ebene verwenden",
"Use selected face as fixed plane": "Ausgewählte Fläche als feste Ebene verwenden",
"Use the SolidWorks mouse mode": "SolidWorks-Mausmodus verwenden",
"User Parameters": "Benutzerparameter",
"VIEW": "ANSICHT",
"VIEWS": "ANSICHTEN",
"Value": "Value",
"Variable Radius": "Variabler Radius",
"Velocity": "Geschwindigkeit",
"Vertices": "Scheitelpunkte",
"View navigator": "Ansichtsnavigator",
@@ -403,10 +502,18 @@
"When": "When",
"Width": "Breite",
"Will fail": "Schlägt fehl",
"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.": "Mit eingeschalteten Mausgesten dreht die rechte Maustaste die Ansicht nicht mehr: Taste gedrückt halten und bewegen öffnet den Werkzeugring, dann die Zone eines Werkzeugs berühren. Ein einfacher Rechtsklick öffnet weiterhin das Menü.",
"X": "X",
"X Angle": "X-Winkel",
"X angle": "X-Winkel",
"X distance": "X-Abstand",
"Y": "Y",
"Y Angle": "Y-Winkel",
"Y angle": "Y-Winkel",
"Y distance": "Y-Abstand",
"Z": "Z",
"Z Angle": "Z-Winkel",
"Z Distance": "Z-Abstand",
"Z angle": "Z-Winkel",
"Z distance": "Z-Abstand",
"Zebra": "Zebra",
@@ -455,8 +562,10 @@
"cmd.component_activate": "Komponente aktivieren",
"cmd.component_break_link": "Verknüpfung trennen",
"cmd.component_capture_position": "Position erfassen",
"cmd.component_copies": "Identische Kopien finden",
"cmd.component_create": "Neue Komponente",
"cmd.component_delete": "Komponente löschen",
"cmd.component_extract": "In Datei extrahieren",
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
"cmd.component_insert_design": "Konstruktion einfügen",
"cmd.component_links": "Verknüpfungen auflisten",
@@ -469,6 +578,7 @@
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
"cmd.component_relink": "Quelle neu verknüpfen",
"cmd.component_rename": "Komponente umbenennen",
"cmd.component_replace_with_link": "Durch Verknüpfung ersetzen",
"cmd.component_revert_position": "Position zurücksetzen",
"cmd.component_unlock": "Inhalt entsperren",
"cmd.component_update_links": "Verknüpfungen aktualisieren",
@@ -859,19 +969,57 @@
"help.mit": "MIT-Lizenz",
"help.notice": "NOTICE",
"help.third_party": "Drittanbieterlizenzen",
"home.back": "Zurück zu {name}",
"home.crashes": "Absturzberichte",
"home.design_workspace": "Entwurf",
"home.documentation": "Dokumentation",
"home.export": "Exportieren",
"home.export_hint": "Wählen Sie ein Format für den aktuellen Entwurf.",
"home.export_json_hint": "Bearbeitbarer Entwurf und Befehlsverlauf",
"home.export_mesh_hint": "Netz zum Drucken oder Teilen",
"home.export_step_hint": "Präzise Volumengeometrie",
"home.import": "Importieren…",
"home.import_hint": "STEP, IGES oder Netz in einen neuen Entwurf",
"home.in_this_browser": "in diesem Browser",
"home.missing": "fehlt",
"home.month_01": "Jan.",
"home.month_02": "Feb.",
"home.month_03": "März",
"home.month_04": "Apr.",
"home.month_05": "Mai",
"home.month_06": "Juni",
"home.month_07": "Juli",
"home.month_08": "Aug.",
"home.month_09": "Sept.",
"home.month_10": "Okt.",
"home.month_11": "Nov.",
"home.month_12": "Dez.",
"home.new_inches": "Neuer Entwurf in Zoll",
"home.new_inches_hint": "Zoll · öffnet sich im Arbeitsbereich Entwurf",
"home.new_mm": "Neuer Entwurf",
"home.new_mm_hint": "Millimeter · öffnet sich im Arbeitsbereich Entwurf",
"home.open": "Öffnen…",
"home.opened_days_ago": "Vor {count} Tag(en) geöffnet",
"home.opened_hours_ago": "Vor {count} Std. geöffnet",
"home.opened_just_now": "Gerade eben geöffnet",
"home.opened_minutes_ago": "Vor {count} Min. geöffnet",
"home.open_hint": "Ein .solidcraft-Entwurf ({shortcut})",
"home.open_now": "Jetzt öffnen",
"home.opened_days_ago": "Vor {count} Tagen",
"home.opened_hours_ago": "Vor {count} Std.",
"home.opened_just_now": "Gerade eben",
"home.opened_minutes_ago": "Vor {count} Min.",
"home.opening": "wird geöffnet…",
"home.properties": "Entwurfseigenschaften",
"home.recent": "Zuletzt verwendet",
"home.recent_empty": "Entwürfe, die Sie öffnen oder speichern, erscheinen hier.",
"home.recovery_count": "Wiederherstellbare Entwürfe: {count}",
"home.recovery_empty": "Keine wiederherstellbaren Entwürfe.",
"home.recovery_hint": "SolidCraft wurde geschlossen, bevor diese Entwürfe gespeichert wurden.",
"home.report_issue": "Problem melden",
"home.samples": "Beispiele",
"home.settings": "Einstellungen",
"home.short_date": "{day}. {month}",
"home.shortcuts": "Tastenkombinationen",
"home.start_hint": "Erstellen Sie einen Entwurf, öffnen Sie eine zuletzt verwendete Datei oder erkunden Sie ein Beispiel.",
"home.whats_new": "Neuigkeiten",
"home.yesterday": "Gestern",
"install.button_tip": "SolidCraft als App installieren: eigenes Fenster, funktioniert auch offline",
"install.dock_1": "Wählen Sie Ablage › Zum Dock hinzufügen.",
"install.dock_2": "Klicken Sie auf „Hinzufügen“.",
@@ -929,8 +1077,15 @@
"motion.to_end": "Zum Ende",
"motion.to_start": "Zum Anfang",
"motion.track_name": "Spur {number}",
"nav.alias.help": "Umschalt+Alt+Linksziehen dreht, Umschalt+Alt+Mitte verschiebt, Umschalt+Alt+Rechtsziehen zoomt; mittleres Ziehen verschiebt ebenfalls und rechtes Ziehen dreht",
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
"nav.free_orbit.label": "Freie Drehung",
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Drehen (Alt/Option halten und bewegen; ohne Maustaste)",
@@ -1026,7 +1181,7 @@
"search.try": "Geben Sie einen Befehlsnamen, ein Merkmal in diesem Design oder einen Parameter ein. Um einen Befehl mit Werten auszuführen, geben Sie seine ID und ein JSON-Objekt ein: {example}",
"search.valid": "Gültig",
"settings.display_quality_note": "Feinere Stufen zeigen gekrümmte Flächen und Kanten glatter und brauchen länger zum Vernetzen. Nur die Ansicht ändert sich, nie das Design oder Exporte.",
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht. Solange sie an ist, wird die Ansicht fortlaufend neu gezeichnet.",
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht.",
"settings.measure_axis_distances": "X/Y/Z-Maße anzeigen",
"settings.measure_axis_distances_help": "Bei geöffneter Messung die Abstände entlang der Weltachsen zusätzlich zum Gesamtabstand anzeigen.",
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
@@ -1149,6 +1304,7 @@
"ui.agent_chat_files_left_out": "Höchstens {max} Dateien pro Nachricht; ausgelassen: {files}",
"ui.agent_chat_files_loading": "Warten, bis die Dateien gelesen sind",
"ui.agent_chat_files_max": "Höchstens {max} Dateien pro Nachricht",
"ui.agent_chat_flatpak_none_found": "Kein Coding-Agent gefunden. Die Flatpak-Sandbox sieht standardmäßig keine Agenten in Ihrem Benutzerordner und hat keinen Netzwerkzugriff. Um Claude Code und seinen ACP-Adapter zu erlauben, die über nvm oder in ~/.local/bin installiert sind, führen Sie diesen Befehl in einem Terminal aus und starten Sie SolidCraft dann neu. Er gewährt Zugriff auf die genannten Programme und Anmeldedateien sowie Netzwerkzugriff:",
"ui.agent_chat_import_failed": "{name}: Der Import ist fehlgeschlagen: {error}",
"ui.agent_chat_intro": "Chatten Sie mit einem Coding-Agenten, den Sie schon nutzen, oder einem Modell-Endpunkt Ihrer Wahl. Er arbeitet über die Werkzeuge von SolidCraft am geöffneten Design: Seine Änderungen erscheinen sofort und lassen sich pro Nachricht in einem Schritt rückgängig machen. Was er ohne Nachfrage darf, legen Sie unter Einstellungen > KI fest.",
"ui.agent_chat_key_in_env": "Schlüssel gesetzt",
@@ -1437,6 +1593,9 @@
"ui.export_keymap_tip": "Tasten, S-Box-Favoriten und die Symbolleiste als Datei",
"ui.extend": "Verlängern",
"ui.extent_type": "Ausdehnungstyp",
"ui.extract_component": "In Datei extrahieren…",
"ui.extract_design_changed": "Das Design hat sich beim Wählen einer Datei geändert. Starten Sie In Datei extrahieren erneut.",
"ui.extract_done": "Die Komponente ist jetzt ein eigenes Design und hier verknüpft.",
"ui.extrude": "Extrudieren",
"ui.factor": "Faktor",
"ui.feature_history": "Feature-Verlauf",
@@ -1468,6 +1627,7 @@
"ui.grid_follows_view_hint": "Seitenansichten zeigen das Raster auf der aufrechten Ebene, die ihnen zugewandt ist, statt auf dem Boden",
"ui.ground_shadow": "Bodenschatten",
"ui.groupSelected": "Auswahl gruppieren",
"ui.hatch": "Schraffur",
"ui.heading_add_ins": "ADD-INS",
"ui.heading_assemble": "ZUSAMMENBAU",
"ui.heading_box": "BLOCK",
@@ -1568,6 +1728,10 @@
"ui.length_decimals": "Längen-Dezimalstellen",
"ui.light": "Hell",
"ui.line": "Linie",
"ui.link_copies": "Identische Kopien verknüpfen",
"ui.link_copies_keep": "Behalten",
"ui.link_copies_link": "Verknüpfen",
"ui.link_copies_text": "Diese Komponenten erzeugen dieselben Körper wie {name}. Sollen sie zu Platzierungen seiner Verknüpfung werden?",
"ui.lock": "Sperren",
"ui.loft": "Loft",
"ui.look_at": "Ansehen",
@@ -1691,6 +1855,7 @@
"ui.render": "Rendern",
"ui.renderer": "Renderer",
"ui.repeat": "Wiederholen",
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
"ui.report_a_bug": "Fehler melden",
"ui.reset": "Zurücksetzen",
"ui.reset_all": "Alle zurücksetzen",
+328 -6
View File
@@ -14,11 +14,15 @@
"Active": "Active",
"Active rule": "Active rule",
"Add": "Add",
"Add Column": "Add Column",
"Add Key": "Add Key",
"Add Mid Radius": "Add Mid Radius",
"Add Row": "Add Row",
"Add a tool": "Add a tool",
"Add condition": "Add condition",
"Add or edit rule": "Add or edit rule",
"Add to New Folder": "Add to New Folder",
"Advanced": "Advanced",
"Against": "Against",
"Alignment": "Alignment",
"All Sketch Points": "All Sketch Points",
@@ -32,6 +36,7 @@
"Appearance name": "Appearance name",
"Applied Radius": "Applied Radius",
"Apply To": "Apply To",
"Apply library appearance": "Apply library appearance",
"Apply to selection": "Apply to selection",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Apply your appearance first. Exports assigned appearances and materials created in this design.",
"Arc length": "Arc length",
@@ -42,13 +47,18 @@
"BLEND SURFACE": "BLEND SURFACE",
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
"BROWSER": "BROWSER",
"Bend Edges": "Bend Edges",
"Bend Line Position": "Bend Line Position",
"Bend Position": "Bend Position",
"Bend Radius": "Bend Radius",
"Bend Table": "Bend Table",
"Bends": "Bends",
"Bodies": "Bodies",
"Bodies (optional)": "Bodies (optional)",
"Body": "Body",
"Body (last if none)": "Body (last if none)",
"Body to split": "Body to split",
"Body, or faces to take out": "Body, or faces to take out",
"Boundary": "Boundary",
"Bounding box": "Bounding box",
"CHECK": "CHECK",
@@ -60,7 +70,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
"Capture All Positions": "Capture All Positions",
"Capture Pos.": "Capture Pos.",
"Catch Depth": "Catch Depth",
"Catch Direction": "Catch Direction",
"Catch Length": "Catch Length",
"Cells": "Cells",
"Center X": "Center X",
"Center Y": "Center Y",
@@ -69,15 +81,21 @@
"Chamfer Type": "Chamfer Type",
"Check collisions": "Check collisions",
"Checked": "Checked",
"Choose Variable radius or Spline in Type.": "Choose Variable radius or Spline in Type.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.",
"Choose…": "Choose…",
"Circular": "Circular",
"Circumference": "Circumference",
"Close draft": "Close draft",
"Collapse": "Collapse",
"Collisions": "Collisions",
"Colour": "Colour",
"Command": "Command",
"Comment": "Comment",
"Component 1": "Component 1",
"Component 2": "Component 2",
"Component1": "Component1",
"Components": "Components",
"Config Table": "Config Table",
"Configuration": "Configuration",
"Configuration rules": "Configuration rules",
@@ -86,6 +104,7 @@
"Contact sets": "Contact sets",
"Continue on GitHub": "Continue on GitHub",
"Continuity": "Continuity",
"Control Radius": "Control Radius",
"Copy Error Message": "Copy Error Message",
"Copy Warning Message": "Copy Warning Message",
"Copy all licences": "Copy all licences",
@@ -95,8 +114,11 @@
"Corner Relief": "Corner Relief",
"Corner Relief Shape": "Corner Relief Shape",
"Counterbore depth": "Counterbore depth",
"Counterbore Ø": "Counterbore Ø",
"Countersink angle": "Countersink angle",
"Countersink Ø": "Countersink Ø",
"Create Issue — review draft": "Create Issue — review draft",
"Create Issue…": "Create Issue…",
"Cross curves": "Cross curves",
"Cube": "Cube",
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
@@ -106,7 +128,11 @@
"Curvature change (G3)": "Curvature change (G3)",
"Custom": "Custom",
"Customize Toolbar": "Customize Toolbar",
"Customize…": "Customize…",
"Cylindrical face": "Cylindrical face",
"Delete": "Delete",
"Delete Selected Features…": "Delete Selected Features…",
"Delete Selected Folders": "Delete Selected Folders",
"Delete all": "Delete all",
"Delete rule": "Delete rule",
"Delete the row": "Delete the row",
@@ -117,8 +143,11 @@
"Describe what happened before continuing.": "Describe what happened before continuing.",
"Designs you open or save appear here.": "Designs you open or save appear here.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
"Diameter": "Diameter",
"Direction": "Direction",
"Discard": "Discard",
"Distance": "Distance",
"Distance 1": "Distance 1",
"Do not export hidden edges": "Do not export hidden edges",
"Don't ask again": "Don't ask again",
"Double-click to rename": "Double-click to rename",
@@ -130,44 +159,57 @@
"Edit Profile Sketch": "Edit Profile Sketch",
"Enable rule": "Enable rule",
"Enabled": "Enabled",
"End Radius": "End Radius",
"End bulge": "End bulge",
"End continuity": "End continuity",
"Expand": "Expand",
"Expected result (optional)": "Expected result (optional)",
"Explode": "Explode",
"Explode Text": "Explode Text",
"Export Animation…": "Export Animation…",
"Export Bend Table…": "Export Bend Table…",
"Export CSV…": "Export CSV…",
"Export Current View as DXF": "Export Current View as DXF",
"Export Current View as DXF…": "Export Current View as DXF…",
"Export DXF": "Export DXF",
"Export DXF…": "Export DXF…",
"Export JSON…": "Export JSON…",
"Export design library...": "Export design library...",
"Export hidden edges as dashed lines": "Export hidden edges as dashed lines",
"Exported by SolidWorks, which usually saves Y up": "Exported by SolidWorks, which usually saves Y up",
"Expression": "Expression",
"Extend to meet": "Extend to meet",
"Extents": "Extents",
"FILES": "FILES",
"FLAT": "FLAT",
"Face": "Face",
"Face Appearance": "Face Appearance",
"Face attachment lost": "Face attachment lost",
"Faces": "Faces",
"Faces / edges / vertices": "Faces / edges / vertices",
"Faces to split": "Faces to split",
"Fastener": "Fastener",
"Favorites": "Favorites",
"Favourite": "Favourite",
"Feature": "Feature",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).",
"Filter": "Filter",
"Filter…": "Filter…",
"Find in Browser": "Find in Browser",
"Finish": "Finish",
"First plane": "First plane",
"Fit and G2 corners require Constant radius.": "Fit and G2 corners require Constant radius.",
"Fixed Face": "Fixed Face",
"Flat Pattern": "Flat Pattern",
"Flat size": "Flat size",
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
"Font": "Font",
"From": "From",
"From Bodies": "From Bodies",
"From edges": "From edges",
"Full Length": "Full Length",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.",
"Gap": "Gap",
"Grid": "Grid",
"HISTORY": "HISTORY",
@@ -175,6 +217,7 @@
"Hidden edges": "Hidden edges",
"History layout": "History layout",
"History names": "History names",
"Hole (optional)": "Hole (optional)",
"Holes": "Holes",
"Holes (edges on them)": "Holes (edges on them)",
"Home view": "Home view",
@@ -185,14 +228,20 @@
"In place (surfaces)": "In place (surfaces)",
"In plane": "In plane",
"Inside": "Inside",
"Instant": "Instant",
"Interface font": "Interface font",
"Interferes": "Interferes",
"Invert": "Invert",
"Isocurve": "Isocurve",
"Issue title": "Issue title",
"Items": "Items",
"JOINTS": "JOINTS",
"Joint Origin1": "Joint Origin1",
"Keep (points on it)": "Keep (points on it)",
"Keep Tools": "Keep Tools",
"Keep of the other surface": "Keep of the other surface",
"Key a joint's value at this step": "Key a joint's value at this step",
"Keys": "Keys",
"LIBRARY": "LIBRARY",
"LINKS": "LINKS",
"Later": "Later",
@@ -203,9 +252,14 @@
"Letter Spacing": "Letter Spacing",
"Library": "Library",
"Library file": "Library file",
"Limits": "Limits",
"Line Spacing": "Line Spacing",
"Line Type": "Line Type",
"Listing the installed fonts…": "Listing the installed fonts…",
"Load library": "Load library",
"Local library": "Local library",
"Loop": "Loop",
"M5 or M5x0.5": "M5 or M5x0.5",
"MANAGE": "MANAGE",
"MATERIALS": "MATERIALS",
"MODEL": "MODEL",
@@ -216,14 +270,20 @@
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
"Material": "Material",
"Max": "Max",
"Maximum": "Maximum",
"Mesh body": "Mesh body",
"Mid Radius": "Mid Radius",
"Middle double-click": "Middle double-click",
"Min": "Min",
"Minimum": "Minimum",
"Mirror Plane": "Mirror Plane",
"Mitre": "Mitre",
"Model Parameters": "Model Parameters",
"Modeled": "Modeled",
"More toolbar panels": "More toolbar panels",
"Motion": "Motion",
"Mouse gestures": "Mouse gestures",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).",
"Move / Copy": "Move / Copy",
"Move Type": "Move Type",
"Move down": "Move down",
@@ -246,8 +306,11 @@
"Not available yet": "Not available yet",
"Notch": "Notch",
"OUTPUT": "OUTPUT",
"Of": "Of",
"On your computer": "On your computer",
"Opacity": "Opacity",
"Open GitHub draft": "Open GitHub draft",
"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-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.",
@@ -269,8 +332,10 @@
"Picked": "Picked",
"Picked point": "Picked point",
"Pitch": "Pitch",
"Pivot": "Pivot",
"Place at the origin instead": "Place at the origin instead",
"Plastic Rules": "Plastic Rules",
"Play the joint's motion in the view": "Play the joint's motion in the view",
"Playback": "Playback",
"Plot": "Plot",
"Position": "Position",
@@ -278,14 +343,18 @@
"Position (G0)": "Position (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other",
"Preferences › Design turns the offer back on": "Preferences › Design turns the offer back on",
"Preview": "Preview",
"Preview Row": "Preview Row",
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
"Profile / Edges": "Profile / Edges",
"Profiles or curves": "Profiles or curves",
"Profiles/Curves": "Profiles/Curves",
"Quantity": "Quantity",
"RECENT": "RECENT",
"REFERENCE": "REFERENCE",
"RULED SURFACE": "RULED SURFACE",
"RULES": "RULES",
"Radius": "Radius",
"Radius Fit": "Radius Fit",
"Ratio": "Ratio",
"Reattach selected face": "Reattach selected face",
@@ -294,6 +363,8 @@
"Rectangular": "Rectangular",
"Reference face": "Reference face",
"Remove Tools": "Remove Tools",
"Remove condition": "Remove condition",
"Remove from Folder": "Remove from Folder",
"Remove from list": "Remove from list",
"Remove override": "Remove override",
"Remove private design details or paths before sharing.": "Remove private design details or paths before sharing.",
@@ -301,6 +372,9 @@
"Remove the column": "Remove the column",
"Remove the track": "Remove the track",
"Remove this point": "Remove this point",
"Rename Feature": "Rename Feature",
"Rename Folder": "Rename Folder",
"Replace existing library file": "Replace existing library file",
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
"Report text copied. Paste it into GitHub and submit your report there.": "Report text copied. Paste it into GitHub and submit your report there.",
"Reports are public. A GitHub account is required.": "Reports are public. A GitHub account is required.",
@@ -309,6 +383,7 @@
"Restore View": "Restore View",
"Restore outer boundary": "Restore outer boundary",
"Restore the inherited component or physical material appearance": "Restore the inherited component or physical material appearance",
"Restore this studio's tools": "Restore this studio's tools",
"Result": "Result",
"Retake screenshot": "Retake screenshot",
"Reverse": "Reverse",
@@ -319,7 +394,13 @@
"Revert Position": "Revert Position",
"Review the report and screenshot before opening GitHub.": "Review the report and screenshot before opening GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Review your report on GitHub and click Submit new issue to publish it.",
"Rib Length": "Rib Length",
"Rib Offset": "Rib Offset",
"Rib Thickness": "Rib Thickness",
"Ribs": "Ribs",
"Rim Edge": "Rim Edge",
"Ring of tools on the right button": "Ring of tools on the right button",
"Roll Back Here": "Roll Back Here",
"Roll History Marker Here": "Roll History Marker Here",
"Round": "Round",
"Rule": "Rule",
@@ -338,13 +419,18 @@
"Search": "Search",
"Search commands": "Search commands",
"Search this library": "Search this library",
"Second plane": "Second plane",
"Section": "Section",
"Sections (one sketch each)": "Sections (one sketch each)",
"Select Other": "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 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 (a face picks its body)",
"Select bodies or faces instead": "Select bodies or faces instead",
"Select edges": "Select edges",
"Selection filter": "Selection filter",
"Semantic faces share their appearance across instances.": "Semantic faces share their appearance across instances.",
"Send your report on GitHub": "Send your report on GitHub",
"Shaded": "Shaded",
"Shape": "Shape",
"Sheet Rules": "Sheet Rules",
"Show Agent Cursor": "Show Agent Cursor",
@@ -362,6 +448,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft closed before these designs were saved. Their last autosave can be opened.",
"Specialise": "Specialise",
"Speed": "Speed",
"Splitting tool": "Splitting tool",
"Stand it up": "Stand it up",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stand it up: a quarter turn about X, as one Move feature you can undo.",
"Stand this part up?": "Stand this part up?",
@@ -372,16 +459,22 @@
"Steps to reproduce (optional)": "Steps to reproduce (optional)",
"Stitch the kept pieces": "Stitch the kept pieces",
"Stitch to both bodies": "Stitch to both bodies",
"Studio": "Studio",
"Study": "Study",
"Style": "Style",
"Summary": "Summary",
"Suppress": "Suppress",
"Suppressed": "Suppressed",
"Surface": "Surface",
"Surface, or faces to turn over": "Surface, or faces to turn over",
"Swap Ends": "Swap Ends",
"TRIM": "TRIM",
"Tangent (G1)": "Tangent (G1)",
"Tangent Chain": "Tangent Chain",
"Tangent chain": "Tangent chain",
"Tap drill diameter": "Tap drill diameter",
"Target": "Target",
"Target Body": "Target Body",
"Tear": "Tear",
"Technical details": "Technical details",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
@@ -396,24 +489,31 @@
"This child is shared. Position changes apply to 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 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.": "This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.",
"To": "To",
"To Offset": "To Offset",
"To edges": "To edges",
"Tolerance": "Tolerance",
"Too close": "Too close",
"Tool Bodies": "Tool Bodies",
"Tools": "Tools",
"Tools, clockwise from the top": "Tools, clockwise from the top",
"Trimming tool (plane, face or curves)": "Trimming tool (plane, face or curves)",
"Turn off section analysis before exporting the view.": "Turn off section analysis before exporting the view.",
"Type": "Type",
"UNTRIM": "UNTRIM",
"Unit": "Unit",
"Unsuppress": "Unsuppress",
"Updating the preview": "Updating the preview",
"Use active author placement": "Use active author placement",
"Use fixed plane": "Use fixed plane",
"Use saved fixed plane": "Use saved fixed plane",
"Use selected face as fixed plane": "Use selected face as fixed plane",
"Use the SolidWorks mouse mode": "Use the SolidWorks mouse mode",
"User Parameters": "User Parameters",
"VIEW": "VIEW",
"VIEWS": "VIEWS",
"Value": "Value",
"Variable Radius": "Variable Radius",
"Velocity": "Velocity",
"Vertices": "Vertices",
"View navigator": "View navigator",
@@ -427,13 +527,44 @@
"When": "When",
"Width": "Width",
"Will fail": "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.": "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 Angle",
"X angle": "X angle",
"X distance": "X distance",
"Y": "Y",
"Y Angle": "Y Angle",
"Y angle": "Y angle",
"Y distance": "Y distance",
"Z": "Z",
"Z Angle": "Z Angle",
"Z Distance": "Z Distance",
"Z angle": "Z angle",
"Z distance": "Z distance",
"Zebra": "Zebra",
"ai.prefs.availability": "Assistant controls",
"ai.prefs.background_commands": "Run agent commands in the background",
"ai.prefs.follow_camera": "Turn the camera to sketches",
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
"ai.prefs.intro": "Agents, permissions and how their work appears.",
"ai.prefs.make_default": "Use by default",
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
"ai.prefs.no_saved_approvals": "No saved approvals.",
"ai.prefs.override_all_warning": "This agent may save, overwrite, export or replace the design without asking.",
"ai.prefs.permission_all": "Allow everything",
"ai.prefs.permission_all_summary": "Available SolidCraft tools run without asking.",
"ai.prefs.permission_ask": "Ask before changes",
"ai.prefs.permission_ask_summary": "Approve every design change and file action.",
"ai.prefs.permission_edits": "Allow model edits",
"ai.prefs.permission_edits_summary": "Model edits run; file and destructive actions still ask.",
"ai.prefs.permission_scope": "For SolidCraft tools only. The agent’s shell and file edits use its own permissions.",
"ai.prefs.pointer_preview_hidden": "Pointer hidden",
"ai.prefs.pointer_preview_speed": "Agent pointer · {speed}",
"ai.prefs.pointer_speed": "Motion speed",
"ai.prefs.privacy_note": "Only your chosen agent or endpoint receives the conversation. Endpoint keys are stored in your system keychain.",
"ai.prefs.show_pointer": "Show agent pointer",
"ai.prefs.turn_limit_hint": "Maximum design-changing tool calls the Assistant may make for one message.",
"angle, height": "angle, height",
"appearance.dielectric_ior": "Dielectric IOR",
"appearance.metallic": "Metallic",
@@ -479,8 +610,10 @@
"cmd.component_activate": "Activate Component",
"cmd.component_break_link": "Break Link",
"cmd.component_capture_position": "Capture Position",
"cmd.component_copies": "Find Identical Copies",
"cmd.component_create": "New Component",
"cmd.component_delete": "Delete Component",
"cmd.component_extract": "Extract to File",
"cmd.component_from_bodies": "Create Components from Bodies",
"cmd.component_insert_design": "Insert Design",
"cmd.component_links": "List Links",
@@ -493,6 +626,7 @@
"cmd.component_properties": "Component Physical Properties",
"cmd.component_relink": "Relink Source",
"cmd.component_rename": "Rename Component",
"cmd.component_replace_with_link": "Replace with Link",
"cmd.component_revert_position": "Revert Position",
"cmd.component_unlock": "Unlock Contents",
"cmd.component_update_links": "Update Links",
@@ -610,6 +744,7 @@
"cmd.motion_plot": "Plot Motion Study",
"cmd.motion_study": "Motion Study",
"cmd.motion_trace": "Motion Trace",
"cmd.motion_trace_edit": "Edit Motion Trace",
"cmd.motion_trace_keep": "Keep Motion Trace",
"cmd.occurrence_copy": "Paste (Instance)",
"cmd.occurrence_ground": "Ground",
@@ -889,19 +1024,57 @@
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
"help.third_party": "Third-party licences",
"home.back": "Back to {name}",
"home.crashes": "Crash reports",
"home.design_workspace": "Design",
"home.documentation": "Documentation",
"home.export": "Export",
"home.export_hint": "Choose a format for the current design.",
"home.export_json_hint": "Editable design and command history",
"home.export_mesh_hint": "Mesh for printing or sharing",
"home.export_step_hint": "Precise solid geometry",
"home.import": "Import…",
"home.import_hint": "STEP, IGES or mesh into a new design",
"home.in_this_browser": "in this browser",
"home.missing": "missing",
"home.month_01": "Jan",
"home.month_02": "Feb",
"home.month_03": "Mar",
"home.month_04": "Apr",
"home.month_05": "May",
"home.month_06": "Jun",
"home.month_07": "Jul",
"home.month_08": "Aug",
"home.month_09": "Sep",
"home.month_10": "Oct",
"home.month_11": "Nov",
"home.month_12": "Dec",
"home.new_inches": "New design, inches",
"home.new_inches_hint": "Inches · opens in Design",
"home.new_mm": "New design",
"home.new_mm_hint": "Millimetres · opens in Design",
"home.open": "Open…",
"home.opened_days_ago": "Opened {count} days ago",
"home.opened_hours_ago": "Opened {count} h ago",
"home.opened_just_now": "Opened just now",
"home.opened_minutes_ago": "Opened {count} min ago",
"home.open_hint": "A .solidcraft design ({shortcut})",
"home.open_now": "Open now",
"home.opened_days_ago": "{count} days ago",
"home.opened_hours_ago": "{count} h ago",
"home.opened_just_now": "Just now",
"home.opened_minutes_ago": "{count} min ago",
"home.opening": "opening…",
"home.properties": "Design properties",
"home.recent": "Recent",
"home.recent_empty": "Designs you open or save appear here.",
"home.recovery_count": "Recoverable designs: {count}",
"home.recovery_empty": "No recoverable designs.",
"home.recovery_hint": "SolidCraft closed before these designs were saved.",
"home.report_issue": "Report an Issue",
"home.samples": "Samples",
"home.settings": "Settings",
"home.short_date": "{day} {month}",
"home.shortcuts": "Keyboard shortcuts",
"home.start_hint": "Create a design, open a recent file, or explore a sample.",
"home.whats_new": "What's new",
"home.yesterday": "Yesterday",
"install.button_tip": "Install SolidCraft as an app: it opens in its own window and works offline",
"install.dock_1": "Choose File › Add to Dock.",
"install.dock_2": "Click Add.",
@@ -959,8 +1132,15 @@
"motion.to_end": "To the end",
"motion.to_start": "To the start",
"motion.track_name": "Track {number}",
"nav.alias.help": "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
"nav.free_orbit.label": "Free orbit",
"nav.fusion.help": "Middle drag pans, Shift+middle orbits, right drag orbits",
"nav.inventor.help": "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
"nav.openscad.help": "Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
"nav.solidworks.help": "Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
"nav.tinkercad.help": "Right drag orbits, middle or Shift+right drag pans",
"nav.touchpad.help": "Hold Shift and move the pointer to pan; hold Alt/Option and move it to orbit. No mouse button is needed. Vertical scroll zooms.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Orbit (hold Alt/Option and move; no mouse button)",
@@ -983,8 +1163,130 @@
"pattern.flip_direction": "Flip direction",
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
"prefs.add_item": "Add the item",
"prefs.alt_frees_manipulators": "Alt / Option also frees manipulator arrows",
"prefs.anti_aliasing_note": "Smooths jagged edges.",
"prefs.applies_on_restart": "Applies on restart",
"prefs.assistant": "Assistant",
"prefs.axes": "Axes",
"prefs.axis": "Axis",
"prefs.bottom": "Bottom",
"prefs.browser": "Browser",
"prefs.built_in": "Built in",
"prefs.button_left": "Left drag",
"prefs.button_middle": "Middle drag",
"prefs.button_right": "Right drag",
"prefs.choose_font": "Choose font file…",
"prefs.click": "Click",
"prefs.contact_shading": "Soft contact shading",
"prefs.contact_shading_note": "Soft shading where parts come close.",
"prefs.context_menu": "Context menu",
"prefs.context_menu_ring": "Context menu · hold for the radial menu",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Open crash reports…",
"prefs.crash_reports_note": "Reports are kept on this computer, with the design and commands that reproduce the problem.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Curved surfaces",
"prefs.curves": "Curved surfaces",
"prefs.dead_zone_note": "The hatched band is the dead zone: motion inside it is ignored.",
"prefs.default": "default",
"prefs.design": "Design defaults",
"prefs.design_note": "What new designs start with. Open designs keep their own.",
"prefs.devices": "3D mouse",
"prefs.display": "Display",
"prefs.edges": "Edges and shading",
"prefs.extend": "Extend",
"prefs.extra_navigation": "Ctrl+left drag on empty space orbits; hold Space to pan.",
"prefs.follows_os": "Follows the OS",
"prefs.font": "Font",
"prefs.font_fallback": "Glyphs the chosen font lacks fall back to the built-in font, so no label shows an empty box.",
"prefs.font_sample": "The quick brown fox · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "The selected font could not be read.",
"prefs.general": "General",
"prefs.general_note": "Language, saving and updates.",
"prefs.gesture_orbit": "Orbit",
"prefs.gesture_pan": "Pan",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Graphics",
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note": "Presets set the choices below together.",
"prefs.history": "History",
"prefs.hold_stop_snapping": "Hold to stop snapping",
"prefs.icons_only": "Icons only",
"prefs.imported_file": "Imported file",
"prefs.input": "Input",
"prefs.interface": "Interface",
"prefs.interface_language": "Interface language",
"prefs.interface_note": "How SolidCraft looks and where its parts sit.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Labels",
"prefs.layout": "Layout",
"prefs.layout_note": "Compact docks panels to the window edges. Browser translucency is available in Floating. Change the Assistant side on the Assistant page.",
"prefs.left": "Left",
"prefs.left_click": "Left click",
"prefs.live_input": "Live input",
"prefs.live_preview": "Live preview",
"prefs.minutes": "minutes",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mouse and trackpad",
"prefs.mouse_heading": "Mouse",
"prefs.mouse_mapping": "Mouse mapping",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "How the mouse, trackpad and 3D mouse move the view.",
"prefs.not_on_gpu": "not on this GPU",
"prefs.on": "On",
"prefs.opaque": "Opaque",
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
"prefs.orientation": "Orientation",
"prefs.overall_speed": "Overall speed",
"prefs.preview_choices": "Preview of your choices",
"prefs.quality_default": "default",
"prefs.quality_fastest": "fastest",
"prefs.quality_slowest": "slowest",
"prefs.quality_smoother": "smoother",
"prefs.reports_saved": "{n} saved",
"prefs.right_click": "Right click",
"prefs.saving": "Autosave and recovery",
"prefs.scale_note": "Also Ctrl + / Ctrl − in the window. Buttons are 28 px at 100%.",
"prefs.scale_text": "Scale and text",
"prefs.select": "Select",
"prefs.select_one_item": "Select one item",
"prefs.selecting": "Selecting",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift adds only; Ctrl / Cmd toggles",
"prefs.shift_add_note": "On: Shift adds, Ctrl / Cmd toggles. Off (default): both toggle.",
"prefs.shortcuts": "Shortcuts",
"prefs.sketch_selection": "Sketch & selection",
"prefs.sketch_selection_heading": "Sketch and selection",
"prefs.sketch_selection_note": "How sketches snap and how clicks select.",
"prefs.sketching": "Sketching",
"prefs.snap_drawing_note": "While drawing or dragging a sketch point.",
"prefs.source": "Source",
"prefs.speed": "Speed",
"prefs.stand_up_note": "After a STEP or IGES file opens with Z up, offer a quarter turn about X.",
"prefs.status_done": "Done",
"prefs.status_error": "Error",
"prefs.status_selected": "Selected",
"prefs.status_warning": "Warning",
"prefs.supersampling_brief": "Draws the view larger and filters it down: very crisp, and heavier.",
"prefs.tab_accent_note": "The accent follows the active tab",
"prefs.theme_file": "theme file",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "now",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Add the item, or take it out again",
"prefs.toolbar": "Toolbar",
"prefs.translucent": "Translucent",
"prefs.under_browser": "Under browser",
"prefs.under_cursor": "Under cursor",
"prefs.units_precision": "Units and precision",
"prefs.updates": "Updates",
"prefs.view": "View behavior",
"prefs.viewport": "Viewport",
"prefs.wheel": "Wheel",
"prefs.wheel_trackpad": "Wheel and trackpad",
"prefs.workspace": "Workspace",
"prefs.zoom_toward_cursor": "Zoom, toward the cursor",
"properties.title": "Properties: {name}",
"recovery.days_ago": "{count} days ago",
"recovery.detail": "{file} · {features} features · autosaved {when}",
@@ -1055,8 +1357,8 @@
"search.suggest": "Did you mean {label}?",
"search.try": "Type a command name, a feature in this design, or a parameter. To run a command with values, type its id and a JSON object: {example}",
"search.valid": "Valid",
"settings.display_quality_note": "Finer levels draw curved faces and edges smoother and take longer to mesh. Only the viewport changes, never the design or exports.",
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner. While it is on, the view redraws continuously.",
"settings.display_quality_note": "Only the display. Designs and exports are not affected.",
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner.",
"settings.measure_axis_distances": "Show X/Y/Z distances",
"settings.measure_axis_distances_help": "Show world-axis component dimensions alongside the total distance while Measure is open.",
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
@@ -1065,6 +1367,7 @@
"settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:",
"settings.reset_title": "Settings were changed",
"settings.supersampling_note": "Draws the view at a higher resolution and filters it down: smoother edges, lines and grid on screens below 4K. It costs GPU time: about 2.25× the pixels at 1.5× and 4× at 2×.",
"sheet.no_body": "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)",
"shortcut.json_error": "parameters must be JSON: {error}",
"shortcut.no_command": "no command `{id}`; the text before the JSON must be a command id",
"shortcut.pin": "Pin to the top of the S box",
@@ -1101,6 +1404,9 @@
"thread.designation": "Designation",
"thread.iso_metric": "ISO metric",
"thread.select_size": "Select size",
"timeline.delete_after_marker_one": "Delete 1 Feature After History Marker",
"timeline.delete_after_marker_other": "Delete {count} Features After History Marker",
"timeline.delete_many": "Delete {count} Selected Features",
"timeline.group": "New Feature Folder",
"timeline.move_many": "Move Timeline Items",
"timeline.move_to_group": "Move Features to Folder",
@@ -1179,6 +1485,7 @@
"ui.agent_chat_files_left_out": "At most {max} files go with one message; left out: {files}",
"ui.agent_chat_files_loading": "Waiting for the files to be read",
"ui.agent_chat_files_max": "At most {max} files on one message",
"ui.agent_chat_flatpak_none_found": "No coding agent found. The Flatpak sandbox cannot see agents installed in your home folder and has no network access by default. To allow Claude Code and its ACP adapter installed through nvm or ~/.local/bin, run this command in a terminal, then restart SolidCraft. It grants access to the listed program and sign-in files, plus network access:",
"ui.agent_chat_import_failed": "{name}: the import failed: {error}",
"ui.agent_chat_intro": "Chat with a coding agent you already use, or a model endpoint you choose. It works on the open design through SolidCraft's tools: its changes show as it makes them and undo as one step per message. What it may do without asking is set in Preferences > AI.",
"ui.agent_chat_key_in_env": "key set",
@@ -1251,8 +1558,10 @@
"ui.ai_add": "Add",
"ui.ai_add_agent": "Add agent…",
"ui.ai_add_agent_title": "Add agent",
"ui.ai_add_anthropic": "Anthropic-compatible API",
"ui.ai_add_api": "OpenAI-compatible API",
"ui.ai_add_custom": "Custom command",
"ui.ai_add_endpoint": "API endpoint",
"ui.ai_add_installed": "Installed agent",
"ui.ai_add_installed_note": "An entry of your own for an installed agent, for example with other arguments. Installed agents are listed by themselves.",
"ui.ai_added": "added",
@@ -1281,6 +1590,7 @@
"ui.ai_key_needs_https": "A key is only sent over HTTPS, or to this computer.",
"ui.ai_key_note": "Stored in the system keychain, never in SolidCraft's settings. Leave it empty for a local server.",
"ui.ai_keychain_error": "The system keychain refused: {error}",
"ui.ai_kind": "Kind",
"ui.ai_mode_all": "Allow everything",
"ui.ai_mode_all_note": "Nothing asks first. Crash reports stay off limits and the folder limits still apply.",
"ui.ai_mode_all_warning": "The agent can save, overwrite, export and replace your design without asking. Use this only with an agent you trust, on designs you have saved.",
@@ -1302,6 +1612,7 @@
"ui.ai_panel": "Panel",
"ui.ai_permissions": "Permissions",
"ui.ai_preset": "Preset",
"ui.ai_protocol": "Protocol",
"ui.ai_remove": "Remove",
"ui.ai_request_limit": "stopped after 24 requests for one message; send another message to go on",
"ui.ai_reset": "Reset",
@@ -1312,6 +1623,8 @@
"ui.ai_test": "Test",
"ui.ai_test_acp_ok": "Answers: {agent}",
"ui.ai_test_acp_tip": "Starts the agent briefly and asks it for a session; ends it again",
"ui.ai_test_answered": "Reached: {model} answered",
"ui.ai_test_anthropic_tip": "Sends a short message to the endpoint",
"ui.ai_test_api_tip": "Asks the endpoint for its models",
"ui.ai_test_no_model": "Reached: {count} models, but not {model}",
"ui.ai_test_ok": "Reached: {count} models",
@@ -1469,6 +1782,9 @@
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
"ui.extend": "Extend",
"ui.extent_type": "Extent Type",
"ui.extract_component": "Extract to File…",
"ui.extract_design_changed": "The design changed while choosing a file. Start Extract to File again.",
"ui.extract_done": "The component is now its own design, linked here.",
"ui.extrude": "Extrude",
"ui.factor": "Factor",
"ui.feature_history": "Feature history",
@@ -1500,6 +1816,7 @@
"ui.grid_follows_view_hint": "Side views show the grid on the upright plane facing them instead of the ground",
"ui.ground_shadow": "Ground shadow",
"ui.groupSelected": "Group Selected",
"ui.hatch": "Hatch",
"ui.heading_add_ins": "ADD-INS",
"ui.heading_assemble": "ASSEMBLE",
"ui.heading_box": "BOX",
@@ -1607,6 +1924,10 @@
"ui.length_decimals": "Length decimals",
"ui.light": "Light",
"ui.line": "Line",
"ui.link_copies": "Link Identical Copies",
"ui.link_copies_keep": "Keep",
"ui.link_copies_link": "Link",
"ui.link_copies_text": "These components build the same bodies as {name}. Make them placements of its link?",
"ui.lock": "Lock",
"ui.loft": "Loft",
"ui.look_at": "Look At",
@@ -1730,6 +2051,7 @@
"ui.render": "Render",
"ui.renderer": "Renderer",
"ui.repeat": "Repeat",
"ui.replace_with_link_done": "The copies are now placements of the linked design.",
"ui.report_a_bug": "Report a bug",
"ui.reset": "Reset",
"ui.reset_all": "Reset All",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Licencia MIT",
"help.notice": "NOTICE",
"help.third_party": "Licencias de terceros",
"home.back": "Volver a {name}",
"home.crashes": "Informes de fallos",
"home.design_workspace": "Diseño",
"home.documentation": "Documentación",
"home.export": "Exportar",
"home.export_hint": "Elige un formato para el diseño actual.",
"home.export_json_hint": "Diseño editable e historial de comandos",
"home.export_mesh_hint": "Malla para imprimir o compartir",
"home.export_step_hint": "Geometría sólida precisa",
"home.import": "Importar…",
"home.import_hint": "STEP, IGES o malla en un nuevo diseño",
"home.in_this_browser": "en este navegador",
"home.missing": "no disponible",
"home.month_01": "ene",
"home.month_02": "feb",
"home.month_03": "mar",
"home.month_04": "abr",
"home.month_05": "may",
"home.month_06": "jun",
"home.month_07": "jul",
"home.month_08": "ago",
"home.month_09": "sept",
"home.month_10": "oct",
"home.month_11": "nov",
"home.month_12": "dic",
"home.new_inches": "Nuevo diseño en pulgadas",
"home.new_inches_hint": "Pulgadas · se abre en Diseño",
"home.new_mm": "Nuevo diseño",
"home.new_mm_hint": "Milímetros · se abre en Diseño",
"home.open": "Abrir…",
"home.opened_days_ago": "Abierto hace {count} días",
"home.opened_hours_ago": "Abierto hace {count} h",
"home.opened_just_now": "Acaba de abrirse",
"home.opened_minutes_ago": "Abierto hace {count} min",
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
"home.open_now": "Abrir ahora",
"home.opened_days_ago": "Hace {count} días",
"home.opened_hours_ago": "Hace {count} h",
"home.opened_just_now": "Ahora mismo",
"home.opened_minutes_ago": "Hace {count} min",
"home.opening": "abriendo…",
"home.properties": "Propiedades del diseño",
"home.recent": "Recientes",
"home.recent_empty": "Los diseños que abras o guardes aparecerán aquí.",
"home.recovery_count": "Diseños recuperables: {count}",
"home.recovery_empty": "No hay diseños recuperables.",
"home.recovery_hint": "SolidCraft se cerró antes de guardar estos diseños.",
"home.report_issue": "Notificar un problema",
"home.samples": "Ejemplos",
"home.settings": "Configuración",
"home.short_date": "{day} {month}",
"home.shortcuts": "Atajos de teclado",
"home.start_hint": "Crea un diseño, abre un archivo reciente o explora un ejemplo.",
"home.whats_new": "Novedades",
"home.yesterday": "Ayer",
"install.button_tip": "Instalar SolidCraft como aplicación: se abre en una ventana propia y funciona sin conexión",
"install.dock_1": "Elige Archivo › Añadir al Dock.",
"install.dock_2": "Haz clic en Añadir.",
@@ -1022,7 +1060,7 @@
"search.try": "Escribe el nombre de un comando, una operación de este diseño o un parámetro. Para ejecutar un comando con valores, escribe su id y un objeto JSON: {example}",
"search.valid": "Válido",
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista. Mientras esté activado, la vista se redibuja continuamente.",
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista.",
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
"settings.measure_axis_distances_help": "Mostrar las cotas de cada eje global junto a la distancia total mientras está abierta la herramienta Medir.",
"settings.msaa_applies_on_restart": "El suavizado de bordes se aplicará la próxima vez que se inicie SolidCraft",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "Se pueden enviar como máximo {max} archivos con un mensaje; omitidos: {files}",
"ui.agent_chat_files_loading": "Esperando a que se lean los archivos",
"ui.agent_chat_files_max": "Como máximo {max} archivos por mensaje",
"ui.agent_chat_flatpak_none_found": "No se ha encontrado ningún agente de programación. De forma predeterminada, el entorno aislado de Flatpak no ve los agentes instalados en tu carpeta personal y no tiene acceso a la red. Para permitir Claude Code y su adaptador ACP instalados con nvm o en ~/.local/bin, ejecuta este comando en un terminal y reinicia SolidCraft. Concede acceso a los programas y archivos de inicio de sesión indicados, además de acceso a la red:",
"ui.agent_chat_import_failed": "{name}: la importación falló: {error}",
"ui.agent_chat_intro": "Habla con un agente de programación que ya utilices o con un servicio de modelos que elijas. Trabaja sobre el diseño abierto mediante las herramientas de SolidCraft: sus cambios se muestran al realizarlos y pueden deshacerse de una vez por mensaje. Lo que puede hacer sin preguntar se configura en Preferencias > IA.",
"ui.agent_chat_key_in_env": "clave configurada",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Las vistas laterales muestran la cuadrícula en el plano vertical que tienen enfrente, en lugar del suelo",
"ui.ground_shadow": "Sombra en el suelo",
"ui.groupSelected": "Agrupar selección",
"ui.hatch": "Rayado",
"ui.heading_add_ins": "COMPLEMENTOS",
"ui.heading_assemble": "ENSAMBLAR",
"ui.heading_box": "CAJA",
+54 -14
View File
@@ -863,19 +863,57 @@
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
"help.third_party": "Licences tierces",
"home.back": "Revenir à {name}",
"home.crashes": "Rapports de plantage",
"home.design_workspace": "Conception",
"home.documentation": "Documentation",
"home.export": "Exporter",
"home.export_hint": "Choisissez un format pour la conception actuelle.",
"home.export_json_hint": "Conception modifiable et historique des commandes",
"home.export_mesh_hint": "Maillage pour l’impression ou le partage",
"home.export_step_hint": "Géométrie solide précise",
"home.import": "Importer…",
"home.import_hint": "STEP, IGES ou maillage dans une nouvelle conception",
"home.in_this_browser": "dans ce navigateur",
"home.missing": "introuvable",
"home.month_01": "janv.",
"home.month_02": "févr.",
"home.month_03": "mars",
"home.month_04": "avr.",
"home.month_05": "mai",
"home.month_06": "juin",
"home.month_07": "juil.",
"home.month_08": "août",
"home.month_09": "sept.",
"home.month_10": "oct.",
"home.month_11": "nov.",
"home.month_12": "déc.",
"home.new_inches": "Nouvelle conception en pouces",
"home.new_inches_hint": "Pouces · ouvre l’espace Conception",
"home.new_mm": "Nouvelle conception",
"home.new_mm_hint": "Millimètres · ouvre l’espace Conception",
"home.open": "Ouvrir…",
"home.opened_days_ago": "Ouvert il y a {count} j",
"home.opened_hours_ago": "Ouvert il y a {count} h",
"home.opened_just_now": "Ouvert à l'instant",
"home.opened_minutes_ago": "Ouvert il y a {count} min",
"home.open_hint": "Une conception .solidcraft ({shortcut})",
"home.open_now": "Ouvrir maintenant",
"home.opened_days_ago": "Il y a {count} j",
"home.opened_hours_ago": "Il y a {count} h",
"home.opened_just_now": "À l’instant",
"home.opened_minutes_ago": "Il y a {count} min",
"home.opening": "ouverture…",
"home.properties": "Propriétés de la conception",
"home.recent": "Récents",
"home.recent_empty": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
"home.recovery_count": "Conceptions récupérables : {count}",
"home.recovery_empty": "Aucune conception récupérable.",
"home.recovery_hint": "SolidCraft s’est fermé avant l’enregistrement de ces conceptions.",
"home.report_issue": "Signaler un problème",
"home.samples": "Exemples",
"home.settings": "Paramètres",
"home.short_date": "{day} {month}",
"home.shortcuts": "Raccourcis clavier",
"home.start_hint": "Créez une conception, ouvrez un fichier récent ou explorez un exemple.",
"home.whats_new": "Nouveautés",
"home.yesterday": "Hier",
"install.button_tip": "Installer SolidCraft comme une app : il s'ouvre dans sa propre fenêtre et fonctionne hors ligne",
"install.dock_1": "Choisissez Fichier › Ajouter au Dock.",
"install.dock_2": "Cliquez sur Ajouter.",
@@ -1006,13 +1044,13 @@
"sample.bracket.name": "Équerre en tôle",
"sample.enclosure.about": "Boîte en coque avec lèvre, bossages de vis, appui et clip",
"sample.enclosure.name": "Boîtier plastique",
"sample.flange.about": "Moyeu et bride avec un réseau circulaire piloté par un paramètre",
"sample.flange.about": "Moyeu et bride avec une répétition circulaire pilotée par un paramètre",
"sample.flange.name": "Bride boulonnée",
"sample.hinge.about": "Deux composants et une liaison pivot, entraînée à 60°",
"sample.hinge.name": "Assemblage de charnière",
"sample.plate.about": "Plaque paramétrique avec congés, perçages et un bossage alésé",
"sample.plate.name": "Plaque d'exemple",
"sample.vents.about": "Rainures en réseau rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.name": "Plaque d'aération",
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
"search.commands": "Commandes",
@@ -1028,7 +1066,7 @@
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
"search.valid": "Valide",
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue. Tant qu'elle est activée, la vue est redessinée en continu.",
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue.",
"settings.measure_axis_distances": "Afficher les cotes X/Y/Z",
"settings.measure_axis_distances_help": "Afficher les cotes suivant les axes du repère global en plus de la distance totale pendant la mesure.",
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
@@ -1151,6 +1189,7 @@
"ui.agent_chat_files_left_out": "Un message ne peut contenir que {max} fichiers au plus ; non joints : {files}",
"ui.agent_chat_files_loading": "Lecture des fichiers en cours",
"ui.agent_chat_files_max": "{max} fichiers au plus par message",
"ui.agent_chat_flatpak_none_found": "Aucun agent de programmation trouvé. Par défaut, le bac à sable Flatpak ne voit pas les agents installés dans votre dossier personnel et n’a pas accès au réseau. Pour autoriser Claude Code et son adaptateur ACP installés avec nvm ou dans ~/.local/bin, exécuter cette commande dans un terminal, puis redémarrer SolidCraft. Elle donne accès aux programmes et fichiers de connexion indiqués, ainsi qu’au réseau :",
"ui.agent_chat_import_failed": "{name} : l'importation a échoué : {error}",
"ui.agent_chat_intro": "Discuter avec un agent de programmation que vous utilisez déjà, ou avec un point d'accès de modèle de votre choix. Il travaille sur la conception ouverte avec les outils de SolidCraft : ses modifications apparaissent au fur et à mesure et s'annulent en une seule étape par message. Ce qu'il peut faire sans demander se règle dans Préférences › IA.",
"ui.agent_chat_key_in_env": "clé définie",
@@ -1335,7 +1374,7 @@
"ui.chamfer_type": "Type de chanfrein",
"ui.change_workspace": "Changer d'espace de travail",
"ui.circle": "Cercle",
"ui.circular_pattern": "Réseau circulaire",
"ui.circular_pattern": "Répétition circulaire",
"ui.clear": "Effacer",
"ui.clear_recent": "Effacer les récents",
"ui.clear_the_selection": "Effacer la sélection",
@@ -1470,11 +1509,12 @@
"ui.grid_follows_view_hint": "Dans les vues de côté, la grille s'affiche sur le plan vertical qui leur fait face plutôt que sur le sol",
"ui.ground_shadow": "Ombre au sol",
"ui.groupSelected": "Grouper la sélection",
"ui.hatch": "Hachures",
"ui.heading_add_ins": "COMPLÉMENTS",
"ui.heading_assemble": "ASSEMBLER",
"ui.heading_box": "BOÎTE",
"ui.heading_chamfer": "CHANFREIN",
"ui.heading_circular_pattern": "RÉSEAU CIRCULAIRE",
"ui.heading_circular_pattern": "RÉPÉTITION CIRCULAIRE",
"ui.heading_coil": "HÉLICE",
"ui.heading_combine": "COMBINER",
"ui.heading_configure": "CONFIGURER",
@@ -1507,14 +1547,14 @@
"ui.heading_offset_plane": "PLAN DÉCALÉ",
"ui.heading_ok": "OK",
"ui.heading_parameters": "PARAMÈTRES",
"ui.heading_pattern": "RÉSEAU",
"ui.heading_pattern_on_path": "RÉSEAU SUR TRAJECTOIRE",
"ui.heading_pattern": "RÉPÉTITION",
"ui.heading_pattern_on_path": "RÉPÉTITION SUR TRAJECTOIRE",
"ui.heading_pipe": "TUYAU",
"ui.heading_plane_at_angle": "PLAN INCLINÉ",
"ui.heading_plastic": "PLASTIQUE",
"ui.heading_position": "POSITION",
"ui.heading_prepare": "PRÉPARER",
"ui.heading_rectangular_pattern": "RÉSEAU RECTANGULAIRE",
"ui.heading_rectangular_pattern": "RÉPÉTITION RECTANGULAIRE",
"ui.heading_revolve": "RÉVOLUTION",
"ui.heading_right": "DROITE",
"ui.heading_save_mesh": "ENREGISTRER COMME MAILLAGE",
@@ -1644,7 +1684,7 @@
"ui.part_size_shared": "Reconstruit la pièce en remplaçant les entités ajoutées. Toutes les occurrences de ce composant sont mises à jour.",
"ui.paste": "Coller",
"ui.path": "Trajectoire",
"ui.pattern_on_path": "Réseau sur trajectoire",
"ui.pattern_on_path": "Répétition sur trajectoire",
"ui.perpendicular": "Perpendicularité",
"ui.perspective_view": "Vue en perspective",
"ui.pinToShortcuts": "Épingler aux raccourcis",
@@ -1681,7 +1721,7 @@
"ui.rail_fit": "Ajustement au rail",
"ui.reachable": "Accessible",
"ui.rectangle": "Rectangle",
"ui.rectangular_pattern": "Réseau rectangulaire",
"ui.rectangular_pattern": "Répétition rectangulaire",
"ui.redo": "Rétablir",
"ui.relink_design_changed": "L’assemblage a changé pendant le choix d’une source. Relancez Relier à nouveau la source.",
"ui.relink_done": "Source reliée à nouveau pour toutes les instances.",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Licenza MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenzas de terceiros",
"home.back": "Volver a {name}",
"home.crashes": "Informes de fallos",
"home.design_workspace": "Deseño",
"home.documentation": "Documentación",
"home.export": "Exportar",
"home.export_hint": "Escolle un formato para o deseño actual.",
"home.export_json_hint": "Deseño editable e historial de comandos",
"home.export_mesh_hint": "Malla para imprimir ou compartir",
"home.export_step_hint": "Xeometría sólida precisa",
"home.import": "Importar…",
"home.import_hint": "STEP, IGES ou malla nun novo deseño",
"home.in_this_browser": "neste navegador",
"home.missing": "non atopado",
"home.month_01": "xan.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "abr.",
"home.month_05": "maio",
"home.month_06": "xuño",
"home.month_07": "xul.",
"home.month_08": "ago.",
"home.month_09": "set.",
"home.month_10": "out.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Novo deseño en polgadas",
"home.new_inches_hint": "Polgadas · ábrese en Deseño",
"home.new_mm": "Novo deseño",
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
"home.open": "Abrir…",
"home.opened_days_ago": "Aberto hai {count} días",
"home.opened_hours_ago": "Aberto hai {count} h",
"home.opened_just_now": "Aberto agora mesmo",
"home.opened_minutes_ago": "Aberto hai {count} min",
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Hai {count} días",
"home.opened_hours_ago": "Hai {count} h",
"home.opened_just_now": "Agora mesmo",
"home.opened_minutes_ago": "Hai {count} min",
"home.opening": "abrindo…",
"home.properties": "Propiedades do deseño",
"home.recent": "Recentes",
"home.recent_empty": "Os deseños que abras ou gardes aparecerán aquí.",
"home.recovery_count": "Deseños recuperables: {count}",
"home.recovery_empty": "Non hai deseños recuperables.",
"home.recovery_hint": "SolidCraft pechouse antes de gardar estes deseños.",
"home.report_issue": "Informar dunha incidencia",
"home.samples": "Exemplos",
"home.settings": "Configuración",
"home.short_date": "{day} {month}",
"home.shortcuts": "Atallos de teclado",
"home.start_hint": "Crea un deseño, abre un ficheiro recente ou explora un exemplo.",
"home.whats_new": "Novidades",
"home.yesterday": "Onte",
"install.button_tip": "Instala SolidCraft como aplicación: ábrese na súa propia xanela e funciona sen conexión",
"install.dock_1": "Escolle Ficheiro › Engadir ao Dock.",
"install.dock_2": "Fai clic en Engadir.",
@@ -1022,7 +1060,7 @@
"search.try": "Escribe o nome dunha orde, unha operación deste deseño ou un parámetro. Para executar unha orde con valores, escribe o seu id e un obxecto JSON: {example}",
"search.valid": "Válido",
"settings.display_quality_note": "Os niveis máis finos debuxan as caras e arestas curvas máis suaves e tardan máis en xerar a malla. Só cambia a vista, nunca o deseño nin as exportacións.",
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista. Mentres está activado, a vista redebúxase continuamente.",
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista.",
"settings.measure_axis_distances": "Mostrar as distancias en X/Y/Z",
"settings.measure_axis_distances_help": "Mostra as compoñentes da distancia segundo os eixes do mundo xunto á distancia total mentres Medir está aberto.",
"settings.msaa_applies_on_restart": "O antialiasing aplicarase a próxima vez que se inicie SolidCraft",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "Cunha mensaxe van como máximo {max} ficheiros; deixados fóra: {files}",
"ui.agent_chat_files_loading": "Agardando a que se lean os ficheiros",
"ui.agent_chat_files_max": "Como máximo {max} ficheiros por mensaxe",
"ui.agent_chat_flatpak_none_found": "Non se atopou ningún axente de programación. De forma predeterminada, o illamento de Flatpak non ve os axentes instalados no teu cartafol persoal e non ten acceso á rede. Para permitir Claude Code e o seu adaptador ACP instalados con nvm ou en ~/.local/bin, executa esta orde nun terminal e despois reinicia SolidCraft. Concede acceso aos programas e ficheiros de inicio de sesión indicados, ademais de acceso á rede:",
"ui.agent_chat_import_failed": "{name}: fallou a importación: {error}",
"ui.agent_chat_intro": "Conversa cun axente de programación que xa uses, ou cun punto de acceso de modelo que escollas. Traballa no deseño aberto a través das ferramentas de SolidCraft: os seus cambios vense mentres os fai e desfanse nun paso por mensaxe. O que pode facer sen preguntar defínese en Preferencias > IA.",
"ui.agent_chat_key_in_env": "clave configurada",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "As vistas laterais mostran a grella no plano vertical que ten diante en vez do chan",
"ui.ground_shadow": "Sombra no chan",
"ui.groupSelected": "Agrupar a selección",
"ui.hatch": "Raiado",
"ui.heading_add_ins": "COMPLEMENTOS",
"ui.heading_assemble": "ENSAMBLAR",
"ui.heading_box": "CAIXA",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "MIT licenc",
"help.notice": "NOTICE",
"help.third_party": "Harmadik felek licencei",
"home.back": "Vissza ide: {name}",
"home.crashes": "Összeomlási jelentések",
"home.design_workspace": "Tervezés",
"home.documentation": "Dokumentáció",
"home.export": "Exportálás",
"home.export_hint": "Válasszon formátumot az aktuális tervhez.",
"home.export_json_hint": "Szerkeszthető terv és parancselőzmények",
"home.export_mesh_hint": "Háló nyomtatáshoz vagy megosztáshoz",
"home.export_step_hint": "Pontos testgeometria",
"home.import": "Importálás…",
"home.import_hint": "STEP, IGES vagy háló importálása új tervbe",
"home.in_this_browser": "ebben a böngészőben",
"home.missing": "hiányzik",
"home.month_01": "jan.",
"home.month_02": "febr.",
"home.month_03": "márc.",
"home.month_04": "ápr.",
"home.month_05": "máj.",
"home.month_06": "jún.",
"home.month_07": "júl.",
"home.month_08": "aug.",
"home.month_09": "szept.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Új terv hüvelykben",
"home.new_inches_hint": "Hüvelyk · a Tervezés munkaterületen nyílik meg",
"home.new_mm": "Új terv",
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
"home.open": "Megnyitás…",
"home.opened_days_ago": "Megnyitva {count} napja",
"home.opened_hours_ago": "Megnyitva {count} órája",
"home.opened_just_now": "Épp most megnyitva",
"home.opened_minutes_ago": "Megnyitva {count} perce",
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
"home.open_now": "Jelenleg megnyitva",
"home.opened_days_ago": "{count} napja",
"home.opened_hours_ago": "{count} órája",
"home.opened_just_now": "Épp most",
"home.opened_minutes_ago": "{count} perce",
"home.opening": "megnyitás…",
"home.properties": "Terv tulajdonságai",
"home.recent": "Legutóbbi",
"home.recent_empty": "A megnyitott vagy mentett tervek itt jelennek meg.",
"home.recovery_count": "Helyreállítható tervek: {count}",
"home.recovery_empty": "Nincs helyreállítható terv.",
"home.recovery_hint": "A SolidCraft a tervek mentése előtt bezárult.",
"home.report_issue": "Hiba jelentése",
"home.samples": "Minták",
"home.settings": "Beállítások",
"home.short_date": "{month} {day}.",
"home.shortcuts": "Billentyűparancsok",
"home.start_hint": "Hozzon létre egy tervet, nyisson meg egy legutóbbi fájlt, vagy fedezzen fel egy mintát.",
"home.whats_new": "Újdonságok",
"home.yesterday": "Tegnap",
"install.button_tip": "A SolidCraft telepítése alkalmazásként: saját ablakban nyílik meg, és offline is működik",
"install.dock_1": "Válassza a Fájl › Hozzáadás a Dockhoz lehetőséget.",
"install.dock_2": "Kattintson a Hozzáadás gombra.",
@@ -1021,7 +1059,7 @@
"search.try": "Írja be egy parancs nevét, a terv egy elemét vagy egy paramétert. Parancs értékekkel való futtatásához írja be az azonosítóját és egy JSON-objektumot: {example}",
"search.valid": "Érvényes",
"settings.display_quality_note": "A finomabb szintek simábban rajzolják az ívelt lapokat és éleket, de tovább tart a hálózás. Csak a nézetablak változik, a terv és az exportok soha.",
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában. Bekapcsolt állapotban a nézet folyamatosan újrarajzolódik.",
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában.",
"settings.measure_axis_distances": "X/Y/Z távolságok megjelenítése",
"settings.measure_axis_distances_help": "A világtengely-összetevők méreteit is megjeleníti a teljes távolság mellett, amíg a Mérés nyitva van.",
"settings.msaa_applies_on_restart": "Az élsimítás a SolidCraft következő indításakor lép érvénybe",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Egy üzenethez legfeljebb {max} fájl tartozhat; kihagyva: {files}",
"ui.agent_chat_files_loading": "Várakozás a fájlok beolvasására",
"ui.agent_chat_files_max": "Egy üzenetben legfeljebb {max} fájl lehet",
"ui.agent_chat_flatpak_none_found": "Nem található kódolóügynök. A Flatpak homokozó alapértelmezés szerint nem látja a saját mappájába telepített ügynököket, és nincs hálózati hozzáférése. Az nvm-mel vagy a ~/.local/bin mappába telepített Claude Code és ACP-adaptere engedélyezéséhez futtassa ezt a parancsot egy terminálban, majd indítsa újra a SolidCraftot. Hozzáférést ad a felsorolt programokhoz és bejelentkezési fájlokhoz, valamint a hálózathoz:",
"ui.agent_chat_import_failed": "{name}: az importálás sikertelen: {error}",
"ui.agent_chat_intro": "Csevegjen egy már használt kódolóügynökkel vagy egy választott modellvégponttal. A megnyitott terven a SolidCraft eszközein keresztül dolgozik: a módosításai láthatók közben, és üzenetenként egy lépésben vonhatók vissza. Hogy mit tehet kérdés nélkül, azt a Beállítások > AI menüpontban adhatja meg.",
"ui.agent_chat_key_in_env": "kulcs beállítva",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Az oldalnézetek a talaj helyett a feléjük néző függőleges síkon mutatják a rácsot",
"ui.ground_shadow": "Talajárnyék",
"ui.groupSelected": "Kijelöltek csoportosítása",
"ui.hatch": "Sraffozás",
"ui.heading_add_ins": "BŐVÍTMÉNYEK",
"ui.heading_assemble": "ÖSSZESZERELÉS",
"ui.heading_box": "DOBOZ",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "Lisensi MIT",
"help.notice": "NOTICE",
"help.third_party": "Lisensi pihak ketiga",
"home.back": "Kembali ke {name}",
"home.crashes": "Laporan kerusakan",
"home.design_workspace": "Desain",
"home.documentation": "Dokumentasi",
"home.export": "Ekspor",
"home.export_hint": "Pilih format untuk desain saat ini.",
"home.export_json_hint": "Desain yang dapat diedit dan riwayat perintah",
"home.export_mesh_hint": "Mesh untuk pencetakan atau berbagi",
"home.export_step_hint": "Geometri benda padat yang presisi",
"home.import": "Impor…",
"home.import_hint": "STEP, IGES, atau mesh ke dalam desain baru",
"home.in_this_browser": "di peramban ini",
"home.missing": "hilang",
"home.month_01": "Jan",
"home.month_02": "Feb",
"home.month_03": "Mar",
"home.month_04": "Apr",
"home.month_05": "Mei",
"home.month_06": "Jun",
"home.month_07": "Jul",
"home.month_08": "Agu",
"home.month_09": "Sep",
"home.month_10": "Okt",
"home.month_11": "Nov",
"home.month_12": "Des",
"home.new_inches": "Desain baru dalam inci",
"home.new_inches_hint": "Inci · terbuka di Desain",
"home.new_mm": "Desain baru",
"home.new_mm_hint": "Milimeter · terbuka di Desain",
"home.open": "Buka…",
"home.opened_days_ago": "Dibuka {count} hari lalu",
"home.opened_hours_ago": "Dibuka {count} jam lalu",
"home.opened_just_now": "Baru saja dibuka",
"home.opened_minutes_ago": "Dibuka {count} mnt lalu",
"home.open_hint": "Desain .solidcraft ({shortcut})",
"home.open_now": "Sedang terbuka",
"home.opened_days_ago": "{count} hari lalu",
"home.opened_hours_ago": "{count} jam lalu",
"home.opened_just_now": "Baru saja",
"home.opened_minutes_ago": "{count} mnt lalu",
"home.opening": "membuka…",
"home.properties": "Properti desain",
"home.recent": "Terbaru",
"home.recent_empty": "Desain yang Anda buka atau simpan akan muncul di sini.",
"home.recovery_count": "Desain yang dapat dipulihkan: {count}",
"home.recovery_empty": "Tidak ada desain yang dapat dipulihkan.",
"home.recovery_hint": "SolidCraft ditutup sebelum desain ini disimpan.",
"home.report_issue": "Laporkan Masalah",
"home.samples": "Contoh",
"home.settings": "Pengaturan",
"home.short_date": "{day} {month}",
"home.shortcuts": "Pintasan papan ketik",
"home.start_hint": "Buat desain, buka berkas terbaru, atau jelajahi contoh.",
"home.whats_new": "Yang baru",
"home.yesterday": "Kemarin",
"install.button_tip": "Pasang SolidCraft sebagai aplikasi: terbuka di jendelanya sendiri dan berfungsi offline",
"install.dock_1": "Pilih File › Add to Dock.",
"install.dock_2": "Klik Add.",
@@ -1021,7 +1059,7 @@
"search.try": "Ketik nama perintah, fitur di desain ini, atau parameter. Untuk menjalankan perintah dengan nilai, ketik id-nya dan objek JSON: {example}",
"search.valid": "Valid",
"settings.display_quality_note": "Level lebih halus menggambar muka dan tepi lengkung lebih mulus dan butuh waktu meshing lebih lama. Hanya viewport yang berubah, tidak pernah desain atau ekspor.",
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan. Saat aktif, tampilan digambar ulang terus-menerus.",
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan.",
"settings.measure_axis_distances": "Tampilkan jarak X/Y/Z",
"settings.measure_axis_distances_help": "Tampilkan dimensi komponen sumbu dunia di samping jarak total saat Ukur terbuka.",
"settings.msaa_applies_on_restart": "Anti-aliasing berlaku saat SolidCraft dimulai berikutnya",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Maksimal {max} file per pesan; tidak disertakan: {files}",
"ui.agent_chat_files_loading": "Menunggu file selesai dibaca",
"ui.agent_chat_files_max": "Maksimal {max} file per pesan",
"ui.agent_chat_flatpak_none_found": "Agen coding tidak ditemukan. Secara bawaan, sandbox Flatpak tidak dapat melihat agen yang terpasang di folder rumah Anda dan tidak memiliki akses jaringan. Untuk mengizinkan Claude Code dan adaptor ACP-nya yang dipasang melalui nvm atau di ~/.local/bin, jalankan perintah ini di terminal, lalu mulai ulang SolidCraft. Perintah ini memberi akses ke program dan berkas masuk yang disebutkan, serta akses jaringan:",
"ui.agent_chat_import_failed": "{name}: impor gagal: {error}",
"ui.agent_chat_intro": "Mengobrol dengan agen coding yang sudah Anda pakai, atau endpoint model pilihan Anda. Agen bekerja pada desain yang terbuka lewat alat SolidCraft: perubahannya tampil saat dibuat dan dapat diurungkan satu langkah per pesan. Yang boleh dilakukannya tanpa bertanya diatur di Preferensi > AI.",
"ui.agent_chat_key_in_env": "key diatur",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Tampilan samping menampilkan grid pada bidang tegak yang menghadapnya, bukan pada dasar",
"ui.ground_shadow": "Bayangan dasar",
"ui.groupSelected": "Kelompokkan yang Dipilih",
"ui.hatch": "Arsir",
"ui.heading_add_ins": "ADD-IN",
"ui.heading_assemble": "RAKIT",
"ui.heading_box": "KOTAK",
+45 -5
View File
@@ -860,19 +860,57 @@
"help.mit": "Licenza MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenze di terze parti",
"home.back": "Torna a {name}",
"home.crashes": "Rapporti di arresto anomalo",
"home.design_workspace": "Progettazione",
"home.documentation": "Documentazione",
"home.export": "Esporta",
"home.export_hint": "Scegli un formato per il progetto attuale.",
"home.export_json_hint": "Progetto modificabile e cronologia dei comandi",
"home.export_mesh_hint": "Mesh per la stampa o la condivisione",
"home.export_step_hint": "Geometria solida precisa",
"home.import": "Importa…",
"home.import_hint": "STEP, IGES o mesh in un nuovo progetto",
"home.in_this_browser": "in questo browser",
"home.missing": "non presente",
"home.month_01": "gen",
"home.month_02": "feb",
"home.month_03": "mar",
"home.month_04": "apr",
"home.month_05": "mag",
"home.month_06": "giu",
"home.month_07": "lug",
"home.month_08": "ago",
"home.month_09": "set",
"home.month_10": "ott",
"home.month_11": "nov",
"home.month_12": "dic",
"home.new_inches": "Nuovo progetto in pollici",
"home.new_inches_hint": "Pollici · si apre in Progettazione",
"home.new_mm": "Nuovo progetto",
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
"home.open": "Apri…",
"home.opened_days_ago": "Aperto {count} giorni fa",
"home.opened_hours_ago": "Aperto {count} h fa",
"home.opened_just_now": "Appena aperto",
"home.opened_minutes_ago": "Aperto {count} min fa",
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
"home.open_now": "Apri ora",
"home.opened_days_ago": "{count} giorni fa",
"home.opened_hours_ago": "{count} h fa",
"home.opened_just_now": "Adesso",
"home.opened_minutes_ago": "{count} min fa",
"home.opening": "apertura…",
"home.properties": "Proprietà del progetto",
"home.recent": "Recenti",
"home.recent_empty": "I progetti che apri o salvi vengono visualizzati qui.",
"home.recovery_count": "Progetti recuperabili: {count}",
"home.recovery_empty": "Nessun progetto recuperabile.",
"home.recovery_hint": "SolidCraft si è chiuso prima del salvataggio di questi progetti.",
"home.report_issue": "Segnala un problema",
"home.samples": "Esempi",
"home.settings": "Impostazioni",
"home.short_date": "{day} {month}",
"home.shortcuts": "Scorciatoie da tastiera",
"home.start_hint": "Crea un progetto, apri un file recente o esplora un esempio.",
"home.whats_new": "Novità",
"home.yesterday": "Ieri",
"install.button_tip": "Installa SolidCraft come applicazione: si apre in una finestra dedicata e funziona offline",
"install.dock_1": "Scegli File › Aggiungi al Dock.",
"install.dock_2": "Fai clic su Aggiungi.",
@@ -1025,7 +1063,7 @@
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
"search.valid": "Valido",
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista. Quando è attivo, la vista viene ridisegnata continuamente.",
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
"settings.msaa_applies_on_restart": "L'antialiasing verrà applicato al prossimo avvio di SolidCraft",
@@ -1148,6 +1186,7 @@
"ui.agent_chat_files_left_out": "Un messaggio può contenere al massimo {max} file; esclusi: {files}",
"ui.agent_chat_files_loading": "In attesa della lettura dei file",
"ui.agent_chat_files_max": "Al massimo {max} file per messaggio",
"ui.agent_chat_flatpak_none_found": "Nessun agente di programmazione trovato. Per impostazione predefinita, la sandbox Flatpak non vede gli agenti installati nella tua cartella home e non ha accesso alla rete. Per consentire Claude Code e il suo adattatore ACP installati con nvm o in ~/.local/bin, esegui questo comando in un terminale, poi riavvia SolidCraft. Concede l’accesso ai programmi e ai file di accesso elencati, oltre all’accesso alla rete:",
"ui.agent_chat_import_failed": "{name}: importazione non riuscita: {error}",
"ui.agent_chat_intro": "Conversa con un agente di programmazione che già usi o con un endpoint di modello a tua scelta. Opera sul progetto aperto tramite gli strumenti di SolidCraft: le modifiche sono visibili mentre vengono eseguite e si annullano in un solo passo per messaggio. Le azioni consentite senza conferma si impostano in Preferenze > IA.",
"ui.agent_chat_key_in_env": "chiave impostata",
@@ -1467,6 +1506,7 @@
"ui.grid_follows_view_hint": "Le viste laterali mostrano la griglia sul piano verticale rivolto verso di esse anziché sul piano di appoggio",
"ui.ground_shadow": "Ombra sul piano di appoggio",
"ui.groupSelected": "Raggruppa selezionati",
"ui.hatch": "Tratteggio",
"ui.heading_add_ins": "COMPONENTI AGGIUNTIVI",
"ui.heading_assemble": "ASSEMBLA",
"ui.heading_box": "PARALLELEPIPEDO",
+328 -6
View File
@@ -14,11 +14,15 @@
"Active": "アクティブ",
"Active rule": "有効なルール",
"Add": "追加",
"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": "すべてのスケッチ点",
@@ -32,6 +36,7 @@
"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": "円弧の長さ",
@@ -42,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": "チェック",
@@ -60,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",
@@ -69,15 +81,21 @@
"Chamfer Type": "面取りタイプ",
"Check collisions": "衝突チェック",
"Checked": "チェック済み",
"Choose Variable radius or Spline in Type.": "タイプで Variable radius またはスプラインを選択してください。",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "新しい平面を選択してください。スケッチジオメトリと寸法は2次元座標系のまま維持されます。",
"Choose…": "選択…",
"Circular": "円形",
"Circumference": "円周",
"Close draft": "下書きを閉じる",
"Collapse": "折りたたむ",
"Collisions": "衝突",
"Colour": "色",
"Command": "コマンド",
"Comment": "コメント",
"Component 1": "コンポーネント 1",
"Component 2": "コンポーネント 2",
"Component1": "コンポーネント1",
"Components": "コンポーネント",
"Config Table": "構成テーブル",
"Configuration": "構成",
"Configuration rules": "構成ルール",
@@ -86,6 +104,7 @@
"Contact sets": "接触セット",
"Continue on GitHub": "GitHubで続ける",
"Continuity": "連続性",
"Control Radius": "制御半径",
"Copy Error Message": "エラーメッセージをコピー",
"Copy Warning Message": "警告メッセージをコピー",
"Copy all licences": "すべてのライセンスをコピー",
@@ -95,8 +114,11 @@
"Corner Relief": "コーナーリリーフ",
"Corner Relief Shape": "コーナーリリーフ形状",
"Counterbore depth": "座ぐり深さ",
"Counterbore Ø": "座ぐり Ø",
"Countersink angle": "皿穴角度",
"Countersink Ø": "皿穴 Ø",
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
"Create Issue…": "Issueを作成…",
"Cross curves": "交差する曲線",
"Cube": "キューブ",
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
@@ -106,7 +128,11 @@
"Curvature change (G3)": "曲率変化 (G3)",
"Custom": "カスタム",
"Customize Toolbar": "ツールバーのカスタマイズ",
"Customize…": "カスタマイズ…",
"Cylindrical face": "円筒面",
"Delete": "削除",
"Delete Selected Features…": "選択したフィーチャーを削除…",
"Delete Selected Folders": "選択したフォルダーを削除",
"Delete all": "すべて削除",
"Delete rule": "ルールを削除",
"Delete the row": "行を削除",
@@ -117,8 +143,11 @@
"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": "ダブルクリックで名前変更",
@@ -130,44 +159,57 @@
"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": "点線で出力",
"Exported by SolidWorks, which usually saves Y up": "SolidWorks で書き出されたファイルです。通常は Y 軸が上になります",
"Expression": "式",
"Extend to meet": "交わるまで延長",
"Extents": "範囲",
"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軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。",
"Filter": "フィルター",
"Filter…": "フィルター…",
"Find in Browser": "ブラウザで検索",
"Finish": "終了",
"First plane": "第1平面",
"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 fit が必要です。G2 コーナーの構築はまだサポートされていません。",
"Gap": "間隔",
"Grid": "グリッド",
"HISTORY": "履歴",
@@ -175,6 +217,7 @@
"Hidden edges": "隠線",
"History layout": "履歴のレイアウト",
"History names": "履歴名",
"Hole (optional)": "穴 (オプション)",
"Holes": "穴",
"Holes (edges on them)": "穴(そのエッジ)",
"Home view": "ホームビュー",
@@ -185,14 +228,20 @@
"In place (surfaces)": "その場で(サーフェス)",
"In plane": "平面内",
"Inside": "内側",
"Instant": "即時",
"Interface font": "インターフェースフォント",
"Interferes": "干渉あり",
"Invert": "反転",
"Isocurve": "等パラメータ線",
"Issue title": "Issueタイトル",
"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": "後で",
@@ -203,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": "モデル",
@@ -216,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": "下へ移動",
@@ -246,8 +306,11 @@
"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.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
"Opens your browser and copies the report text for you to paste into GitHub.": "ブラウザーを開き、GitHubに貼り付ける報告本文をコピーします。",
@@ -269,8 +332,10 @@
"Picked": "選択したもの",
"Picked point": "選択した点",
"Pitch": "ピッチ",
"Pivot": "ピボット",
"Place at the origin instead": "代わりに原点に配置",
"Plastic Rules": "樹脂ルール",
"Play the joint's motion in the view": "ビューでジョイントの動きを再生",
"Playback": "再生",
"Plot": "グラフ",
"Position": "位置",
@@ -278,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": "選択した面を再アタッチ",
@@ -294,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.": "共有前に、非公開の設計情報やファイルパスを削除してください。",
@@ -301,6 +372,9 @@
"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アカウントが必要です。",
@@ -309,6 +383,7 @@
"Restore View": "ビューを復元",
"Restore outer boundary": "外側の境界を復元",
"Restore the inherited component or physical material appearance": "コンポーネントまたは物理マテリアルから継承した外観に戻す",
"Restore this studio's tools": "このスタジオのツールを復元",
"Result": "結果",
"Retake screenshot": "スクリーンショットを再取得",
"Reverse": "反転",
@@ -319,7 +394,13 @@
"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": "ルール",
@@ -338,13 +419,18 @@
"Search": "検索",
"Search commands": "コマンドを検索",
"Search this library": "このライブラリを検索",
"Second plane": "第2平面",
"Section": "断面",
"Sections (one sketch each)": "セクション (各1スケッチ)",
"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": "エージェントカーソルを表示",
@@ -362,6 +448,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "これらのデザインが保存される前にSolidCraftが終了しました。最後の自動保存ファイルを開くことができます。",
"Specialise": "特殊操作",
"Speed": "速度",
"Splitting tool": "分割ツール",
"Stand it up": "正立させる",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "正立させる: 取り消し可能な1つの移動フィーチャーとして、X軸周りに90度回転します。",
"Stand this part up?": "このパーツを正立させますか?",
@@ -372,16 +459,22 @@
"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.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
@@ -396,24 +489,31 @@
"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": "ビューナビゲーター",
@@ -427,13 +527,44 @@
"When": "条件 (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": "アシスタントが1件のメッセージで行える、設計を変更するツール呼び出しの上限です。",
"angle, height": "角度, 高さ",
"appearance.dielectric_ior": "誘電体の屈折率",
"appearance.metallic": "金属度",
@@ -479,8 +610,10 @@
"cmd.component_activate": "コンポーネントをアクティブ化",
"cmd.component_break_link": "リンクを解除",
"cmd.component_capture_position": "位置をキャプチャ",
"cmd.component_copies": "同一のコピーを検索",
"cmd.component_create": "新規コンポーネント",
"cmd.component_delete": "コンポーネントを削除",
"cmd.component_extract": "ファイルに抽出",
"cmd.component_from_bodies": "ボディからコンポーネントを作成",
"cmd.component_insert_design": "デザインを挿入",
"cmd.component_links": "リンクを一覧表示",
@@ -493,6 +626,7 @@
"cmd.component_properties": "コンポーネントの物理的プロパティ",
"cmd.component_relink": "参照元を再リンク",
"cmd.component_rename": "コンポーネントの名前を変更",
"cmd.component_replace_with_link": "リンクに置き換え",
"cmd.component_revert_position": "位置を元に戻す",
"cmd.component_unlock": "内容のロックを解除",
"cmd.component_update_links": "リンクを更新",
@@ -610,6 +744,7 @@
"cmd.motion_plot": "モーション スタディのグラフ",
"cmd.motion_study": "モーション スタディ",
"cmd.motion_trace": "モーション トレース",
"cmd.motion_trace_edit": "モーション トレースを編集",
"cmd.motion_trace_keep": "モーション トレースを保持",
"cmd.occurrence_copy": "貼り付け(インスタンス)",
"cmd.occurrence_ground": "空間固定",
@@ -889,19 +1024,57 @@
"help.solid.revolve": "スケッチの輪郭を軸のまわりに回転させ、丸い立体を作ります。軸、つまみ、ボウルなど、中心線まわりに丸い部品に使います。",
"help.solid.shell": "立体の中をくり抜き、指定した厚さの壁を残します。選んだ面を開口にすることもできます。箱、カバーなどの薄肉の部品に使います。",
"help.third_party": "サードパーティライセンス",
"home.back": "{name} に戻る",
"home.crashes": "クラッシュレポート",
"home.design_workspace": "デザイン",
"home.documentation": "ドキュメント",
"home.export": "エクスポート",
"home.export_hint": "現在のデザインの出力形式を選択してください。",
"home.export_json_hint": "編集可能なデザインとコマンド履歴",
"home.export_mesh_hint": "印刷や共有用のメッシュ",
"home.export_step_hint": "精密なソリッド形状",
"home.import": "インポート…",
"home.import_hint": "STEP、IGES、またはメッシュを新しいデザインに読み込みます",
"home.in_this_browser": "このブラウザ内",
"home.missing": "見つかりません",
"home.month_01": "1月",
"home.month_02": "2月",
"home.month_03": "3月",
"home.month_04": "4月",
"home.month_05": "5月",
"home.month_06": "6月",
"home.month_07": "7月",
"home.month_08": "8月",
"home.month_09": "9月",
"home.month_10": "10月",
"home.month_11": "11月",
"home.month_12": "12月",
"home.new_inches": "新しいデザイン(インチ)",
"home.new_inches_hint": "インチ · デザインで開きます",
"home.new_mm": "新しいデザイン",
"home.new_mm_hint": "ミリメートル · デザインで開きます",
"home.open": "開く…",
"home.opened_days_ago": "{count} 日前に開きました",
"home.opened_hours_ago": "{count} 時間前に開きました",
"home.opened_just_now": "たった今開きました",
"home.opened_minutes_ago": "{count} 分前に開きました",
"home.open_hint": ".solidcraft 形式のデザイン ({shortcut})",
"home.open_now": "今すぐ開く",
"home.opened_days_ago": "{count} 日前",
"home.opened_hours_ago": "{count} 時間前",
"home.opened_just_now": "たった今",
"home.opened_minutes_ago": "{count} 分前",
"home.opening": "開いています…",
"home.properties": "デザインのプロパティ",
"home.recent": "最近使用した項目",
"home.recent_empty": "開いたり保存したデザインがここに表示されます。",
"home.recovery_count": "復元可能なデザイン: {count} 件",
"home.recovery_empty": "復元可能なデザインはありません。",
"home.recovery_hint": "これらのデザインが保存される前に SolidCraft が終了しました。",
"home.report_issue": "問題を報告",
"home.samples": "サンプル",
"home.settings": "設定",
"home.short_date": "{month}{day}日",
"home.shortcuts": "キーボードショートカット",
"home.start_hint": "デザインを作成するか、最近使ったファイルを開くか、サンプルをご覧ください。",
"home.whats_new": "新機能",
"home.yesterday": "昨日",
"install.button_tip": "SolidCraft をアプリとしてインストールします。専用のウィンドウで開き、オフラインでも使えます",
"install.dock_1": "「ファイル」›「Dockに追加」を選びます。",
"install.dock_2": "「追加」をクリックします。",
@@ -959,8 +1132,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マウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
"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 を押しながら移動、マウスボタン不要)",
@@ -983,8 +1163,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": "1つまたは2つの頂点、エッジ、面を選択",
"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": "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": "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 − でも変更できます。100% のボタンは 28 px です。",
"prefs.scale_text": "拡大率と文字",
"prefs.select": "選択",
"prefs.select_one_item": "項目を 1 つ選択",
"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 軸周りに 90° 回転するか確認します。",
"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}",
@@ -1055,8 +1357,8 @@
"search.suggest": "{label} ですか?",
"search.try": "コマンド名、このデザインのフィーチャー、またはパラメーターを入力します。値を指定して実行するには、IDとJSONオブジェクトを入力します:{example}",
"search.valid": "有効",
"settings.display_quality_note": "細かいほど曲面とエッジが滑らかに表示されますが、メッシュの生成に時間がかかります。変わるのはビューポートだけで、設計やエクスポートは変わりません。",
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。オンの間、ビューは常に再描画されます。",
"settings.display_quality_note": "表示だけに影響します。設計や書き出しは変わりません。",
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。",
"settings.measure_axis_distances": "計測のX・Y・Z距離を表示",
"settings.measure_axis_distances_help": "計測ダイアログを開いている間、2点間距離に加えてワールドX・Y・Z軸方向の寸法線と距離を表示します。",
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
@@ -1065,6 +1367,7 @@
"settings.reset_intro": "次の設定はこのコンピューターでは使用できず、SolidCraft が起動できなくなるため変更されました:",
"settings.reset_title": "設定が変更されました",
"settings.supersampling_note": "ビューを高い解像度で描画してから縮小します。4K 未満の画面でエッジ、線、グリッドが滑らかになります。GPU の負荷が増えます(1.5× で約 2.25 倍、2× で 4 倍のピクセル)。",
"sheet.no_body": "板金ボディがありません。フランジ > Base で作成するか、板金に変換でプレートを板金に変換してください(広い平面を選択)。",
"shortcut.json_error": "パラメータはJSON形式で指定してください: {error}",
"shortcut.no_command": "コマンド`{id}`がありません。JSONの前のテキストはコマンドIDである必要があります",
"shortcut.pin": "Sボックスの先頭にピン留め",
@@ -1101,6 +1404,9 @@
"thread.designation": "ねじの呼び",
"thread.iso_metric": "ISOメートルねじ",
"thread.select_size": "サイズを選択",
"timeline.delete_after_marker_one": "履歴マーカー以降の 1 個のフィーチャーを削除",
"timeline.delete_after_marker_other": "履歴マーカー以降の {count} 個のフィーチャーを削除",
"timeline.delete_many": "選択した {count} 個のフィーチャーを削除",
"timeline.group": "フィーチャーフォルダを新規作成",
"timeline.move_many": "タイムライン項目を移動",
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
@@ -1179,6 +1485,7 @@
"ui.agent_chat_files_left_out": "1 つのメッセージで送れるファイルは {max} 個までです。送らなかったファイル: {files}",
"ui.agent_chat_files_loading": "ファイルの読み込みを待っています",
"ui.agent_chat_files_max": "1 つのメッセージに添付できるのは {max} 個までです",
"ui.agent_chat_flatpak_none_found": "コーディングエージェントが見つかりません。Flatpak のサンドボックスは、既定ではホームフォルダー内のエージェントにアクセスできず、ネットワークにも接続できません。nvm または ~/.local/bin にインストールした Claude Code と ACP アダプターを許可するには、端末で次のコマンドを実行し、SolidCraft を再起動してください。指定したプログラムとログインファイルへのアクセス、およびネットワーク接続を許可します:",
"ui.agent_chat_import_failed": "{name}: インポートに失敗しました: {error}",
"ui.agent_chat_intro": "普段使っているコーディングエージェント、または選んだモデルのエンドポイントとチャットできます。SolidCraft のツールで開いているデザインを操作し、変更はその場で表示され、メッセージごとに1回で元に戻せます。確認なしで許可する範囲は「環境設定 > AI」で設定します。",
"ui.agent_chat_key_in_env": "キー設定済み",
@@ -1251,8 +1558,10 @@
"ui.ai_add": "追加",
"ui.ai_add_agent": "エージェントを追加…",
"ui.ai_add_agent_title": "エージェントを追加",
"ui.ai_add_anthropic": "Anthropic 互換 API",
"ui.ai_add_api": "OpenAI 互換 API",
"ui.ai_add_custom": "カスタムコマンド",
"ui.ai_add_endpoint": "API エンドポイント",
"ui.ai_add_installed": "インストール済みエージェント",
"ui.ai_add_installed_note": "インストール済みエージェントの独自エントリ(例: 別の引数で起動)。インストール済みのエージェントは自動で一覧に表示されます。",
"ui.ai_added": "追加済み",
@@ -1281,6 +1590,7 @@
"ui.ai_key_needs_https": "キーは HTTPS 経由、またはこのコンピューター宛てにのみ送信されます。",
"ui.ai_key_note": "システムのキーチェーンに保存され、SolidCraft の設定には保存されません。ローカルサーバーの場合は空欄のままにします。",
"ui.ai_keychain_error": "システムのキーチェーンが拒否しました: {error}",
"ui.ai_kind": "種類",
"ui.ai_mode_all": "すべて許可",
"ui.ai_mode_all_note": "確認なしで実行します。クラッシュレポートは引き続き対象外で、フォルダー制限も有効です。",
"ui.ai_mode_all_warning": "エージェントは確認なしでデザインの保存・上書き・エクスポート・置き換えができます。信頼できるエージェントで、保存済みのデザインに対してのみ使ってください。",
@@ -1302,6 +1612,7 @@
"ui.ai_panel": "パネル",
"ui.ai_permissions": "権限",
"ui.ai_preset": "プリセット",
"ui.ai_protocol": "プロトコル",
"ui.ai_remove": "削除",
"ui.ai_request_limit": "1メッセージあたり24リクエストで停止しました。続けるには、もう一度メッセージを送ってください",
"ui.ai_reset": "リセット",
@@ -1312,6 +1623,8 @@
"ui.ai_test": "テスト",
"ui.ai_test_acp_ok": "応答あり: {agent}",
"ui.ai_test_acp_tip": "エージェントを短時間起動してセッションを要求し、終了します",
"ui.ai_test_answered": "接続成功: {model} が応答しました",
"ui.ai_test_anthropic_tip": "エンドポイントに短いメッセージを送信します",
"ui.ai_test_api_tip": "エンドポイントにモデル一覧を問い合わせます",
"ui.ai_test_no_model": "接続成功: {count} 個のモデル({model} はありません)",
"ui.ai_test_ok": "接続成功: {count} 個のモデル",
@@ -1469,6 +1782,9 @@
"ui.export_keymap_tip": "ショートカット、Sボックスのお気に入り、ツールバー設定をファイルに保存",
"ui.extend": "延長",
"ui.extent_type": "範囲タイプ",
"ui.extract_component": "ファイルに抽出…",
"ui.extract_design_changed": "ファイルの選択中にデザインが変更されました。ファイルへの抽出をやり直してください。",
"ui.extract_done": "コンポーネントは独立したデザインになり、ここにリンクされました。",
"ui.extrude": "押し出し",
"ui.factor": "倍率",
"ui.feature_history": "フィーチャー履歴",
@@ -1500,6 +1816,7 @@
"ui.grid_follows_view_hint": "側面ビューでは、グリッドを地面ではなく、そのビューに向いた垂直平面に表示します",
"ui.ground_shadow": "地面の影",
"ui.groupSelected": "選択をグループ化",
"ui.hatch": "ハッチング",
"ui.heading_add_ins": "アドイン",
"ui.heading_assemble": "アセンブリ",
"ui.heading_box": "直方体",
@@ -1607,6 +1924,10 @@
"ui.length_decimals": "長さの小数点以下の桁数",
"ui.light": "ライト",
"ui.line": "線分",
"ui.link_copies": "同一のコピーをリンク",
"ui.link_copies_keep": "そのまま",
"ui.link_copies_link": "リンク",
"ui.link_copies_text": "これらのコンポーネントは {name} と同じボディを作成します。そのリンクの配置にしますか?",
"ui.lock": "ロック",
"ui.loft": "ロフト",
"ui.look_at": "正面視",
@@ -1730,6 +2051,7 @@
"ui.render": "レンダリング",
"ui.renderer": "レンダラー",
"ui.repeat": "繰り返し",
"ui.replace_with_link_done": "コピーはリンクされたデザインの配置になりました。",
"ui.report_a_bug": "不具合を報告",
"ui.reset": "リセット",
"ui.reset_all": "すべてリセット",
+45 -5
View File
@@ -859,19 +859,57 @@
"help.mit": "MIT 라이선스",
"help.notice": "NOTICE",
"help.third_party": "타사 라이선스",
"home.back": "{name}(으)로 돌아가기",
"home.crashes": "충돌 보고서",
"home.design_workspace": "설계",
"home.documentation": "문서",
"home.export": "내보내기",
"home.export_hint": "현재 설계의 형식을 선택하세요.",
"home.export_json_hint": "편집 가능한 설계 및 명령 기록",
"home.export_mesh_hint": "인쇄 또는 공유용 메시",
"home.export_step_hint": "정밀한 솔리드 형상",
"home.import": "가져오기…",
"home.import_hint": "STEP, IGES 또는 메시를 새 설계로 가져오기",
"home.in_this_browser": "이 브라우저에 저장됨",
"home.missing": "찾을 수 없음",
"home.month_01": "1월",
"home.month_02": "2월",
"home.month_03": "3월",
"home.month_04": "4월",
"home.month_05": "5월",
"home.month_06": "6월",
"home.month_07": "7월",
"home.month_08": "8월",
"home.month_09": "9월",
"home.month_10": "10월",
"home.month_11": "11월",
"home.month_12": "12월",
"home.new_inches": "새 설계(인치)",
"home.new_inches_hint": "인치 · 설계 작업 공간에서 열기",
"home.new_mm": "새 설계",
"home.new_mm_hint": "밀리미터 · 설계 작업 공간에서 열기",
"home.open": "열기…",
"home.opened_days_ago": "{count}일 전에 열림",
"home.opened_hours_ago": "{count}시간 전에 열림",
"home.opened_just_now": "방금 열림",
"home.opened_minutes_ago": "{count}분 전에 열림",
"home.open_hint": ".solidcraft 설계 ({shortcut})",
"home.open_now": "지금 열기",
"home.opened_days_ago": "{count}일 전",
"home.opened_hours_ago": "{count}시간 전",
"home.opened_just_now": "방금",
"home.opened_minutes_ago": "{count}분 전",
"home.opening": "여는 중…",
"home.properties": "설계 속성",
"home.recent": "최근 항목",
"home.recent_empty": "열거나 저장한 설계가 여기에 표시됩니다.",
"home.recovery_count": "복구 가능한 설계: {count}개",
"home.recovery_empty": "복구 가능한 설계가 없습니다.",
"home.recovery_hint": "이 설계가 저장되기 전에 SolidCraft가 종료되었습니다.",
"home.report_issue": "문제 신고",
"home.samples": "샘플",
"home.settings": "설정",
"home.short_date": "{month} {day}일",
"home.shortcuts": "키보드 단축키",
"home.start_hint": "설계를 만들거나, 최근 파일을 열거나, 예제를 살펴보세요.",
"home.whats_new": "새로운 기능",
"home.yesterday": "어제",
"install.button_tip": "SolidCraft를 앱으로 설치합니다. 별도 창에서 열리며 오프라인에서도 작동합니다",
"install.dock_1": "파일 › Dock에 추가를 선택합니다.",
"install.dock_2": "추가를 클릭합니다.",
@@ -1026,7 +1064,7 @@
"search.try": "명령 이름, 이 디자인의 피처 또는 매개변수를 입력하세요. 값과 함께 명령을 실행하려면 명령 ID와 JSON 객체를 입력하세요: {example}",
"search.valid": "올바름",
"settings.display_quality_note": "세밀할수록 곡면과 모서리가 더 매끄럽게 표시되지만 메시 생성 시간이 늘어납니다. 뷰포트만 바뀌며 설계나 내보내기는 바뀌지 않습니다.",
"settings.frame_rate_note": "뷰의 오른쪽 아래에 초당 프레임 수를 표시합니다. 켜져 있는 동안 뷰를 계속 다시 그립니다.",
"settings.frame_rate_note": "뷰의 오른쪽 아래에 초당 프레임 수를 표시합니다.",
"settings.measure_axis_distances": "X/Y/Z 거리 표시",
"settings.measure_axis_distances_help": "측정 대화상자가 열려 있는 동안 전체 거리와 함께 월드 축 방향 치수와 거리를 표시합니다.",
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
@@ -1149,6 +1187,7 @@
"ui.agent_chat_files_left_out": "메시지 하나에는 파일을 최대 {max}개까지 보낼 수 있습니다. 제외됨: {files}",
"ui.agent_chat_files_loading": "파일을 읽는 동안 기다리는 중",
"ui.agent_chat_files_max": "메시지 하나에 파일은 최대 {max}개",
"ui.agent_chat_flatpak_none_found": "코딩 에이전트를 찾지 못했습니다. Flatpak 샌드박스는 기본적으로 홈 폴더에 설치된 에이전트를 볼 수 없고 네트워크에도 접근할 수 없습니다. nvm 또는 ~/.local/bin에 설치한 Claude Code와 ACP 어댑터를 허용하려면 터미널에서 다음 명령을 실행한 뒤 SolidCraft를 다시 시작하세요. 나열된 프로그램과 로그인 파일에 대한 접근 권한 및 네트워크 접근 권한을 부여합니다:",
"ui.agent_chat_import_failed": "{name}: 가져오기 실패: {error}",
"ui.agent_chat_intro": "이미 사용 중인 코딩 에이전트나 선택한 모델 엔드포인트와 채팅하세요. SolidCraft 도구로 열린 디자인을 다루며, 변경 사항은 바로 보이고 메시지마다 한 번에 실행 취소할 수 있습니다. 묻지 않고 할 수 있는 일은 환경 설정 > AI에서 정합니다.",
"ui.agent_chat_key_in_env": "키 설정됨",
@@ -1468,6 +1507,7 @@
"ui.grid_follows_view_hint": "측면 뷰에서는 지면 대신 마주 보는 수직 평면에 격자를 표시합니다",
"ui.ground_shadow": "바닥 그림자",
"ui.groupSelected": "선택 항목 그룹화",
"ui.hatch": "해칭",
"ui.heading_add_ins": "애드인",
"ui.heading_assemble": "조립",
"ui.heading_box": "박스",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Lesen MIT",
"help.notice": "NOTIS",
"help.third_party": "Lesen pihak ketiga",
"home.back": "Kembali ke {name}",
"home.crashes": "Laporan ranap",
"home.design_workspace": "Reka Bentuk",
"home.documentation": "Dokumentasi",
"home.export": "Eksport",
"home.export_hint": "Pilih format untuk reka bentuk semasa.",
"home.export_json_hint": "Reka bentuk boleh sunting dan sejarah perintah",
"home.export_mesh_hint": "Jejaring untuk pencetakan atau perkongsian",
"home.export_step_hint": "Geometri pepejal yang tepat",
"home.import": "Import…",
"home.import_hint": "STEP, IGES atau jejaring ke dalam reka bentuk baharu",
"home.in_this_browser": "dalam pelayar ini",
"home.missing": "tiada",
"home.month_01": "Jan",
"home.month_02": "Feb",
"home.month_03": "Mac",
"home.month_04": "Apr",
"home.month_05": "Mei",
"home.month_06": "Jun",
"home.month_07": "Jul",
"home.month_08": "Ogo",
"home.month_09": "Sep",
"home.month_10": "Okt",
"home.month_11": "Nov",
"home.month_12": "Dis",
"home.new_inches": "Reka bentuk baharu dalam inci",
"home.new_inches_hint": "Inci · dibuka dalam Reka Bentuk",
"home.new_mm": "Reka bentuk baharu",
"home.new_mm_hint": "Milimeter · dibuka dalam Reka Bentuk",
"home.open": "Buka…",
"home.opened_days_ago": "Dibuka {count} hari lalu",
"home.opened_hours_ago": "Dibuka {count} jam lalu",
"home.opened_just_now": "Baru dibuka",
"home.opened_minutes_ago": "Dibuka {count} minit lalu",
"home.open_hint": "Reka bentuk .solidcraft ({shortcut})",
"home.open_now": "Buka sekarang",
"home.opened_days_ago": "{count} hari lalu",
"home.opened_hours_ago": "{count} jam lalu",
"home.opened_just_now": "Baru sahaja",
"home.opened_minutes_ago": "{count} minit lalu",
"home.opening": "membuka…",
"home.properties": "Sifat reka bentuk",
"home.recent": "Terkini",
"home.recent_empty": "Reka bentuk yang anda buka atau simpan dipaparkan di sini.",
"home.recovery_count": "Reka bentuk boleh dipulihkan: {count}",
"home.recovery_empty": "Tiada reka bentuk yang boleh dipulihkan.",
"home.recovery_hint": "SolidCraft ditutup sebelum reka bentuk ini disimpan.",
"home.report_issue": "Laporkan Isu",
"home.samples": "Contoh",
"home.settings": "Tetapan",
"home.short_date": "{day} {month}",
"home.shortcuts": "Pintasan papan kekunci",
"home.start_hint": "Cipta reka bentuk, buka fail terkini atau terokai contoh.",
"home.whats_new": "Apa yang baharu",
"home.yesterday": "Semalam",
"install.button_tip": "Pasang SolidCraft: tetingkap sendiri dan boleh guna di luar talian",
"install.dock_1": "Pilih File › Add to Dock.",
"install.dock_2": "Klik Add.",
@@ -1022,7 +1060,7 @@
"search.try": "Taip nama arahan, ciri dalam reka bentuk ini, atau parameter. Untuk menjalankan arahan dengan nilai, taip ID-nya dan objek JSON: {example}",
"search.valid": "Sah",
"settings.display_quality_note": "Tahap halus melicinkan muka dan tepi melengkung, tetapi mesh lebih lambat disiapkan. Hanya paparan berubah, bukan reka bentuk atau eksport.",
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah. Jika diaktifkan, pandangan dilukis semula tanpa henti.",
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah.",
"settings.measure_axis_distances": "Papar jarak X/Y/Z",
"settings.measure_axis_distances_help": "Papar jarak paksi dunia X/Y/Z bersama jarak keseluruhan semasa Ukur dibuka.",
"settings.msaa_applies_on_restart": "Antialias digunakan apabila SolidCraft dimulakan semula",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "Paling banyak {max} fail bersama satu mesej; ditinggalkan: {files}",
"ui.agent_chat_files_loading": "Menunggu fail dibaca",
"ui.agent_chat_files_max": "Paling banyak {max} fail pada satu mesej",
"ui.agent_chat_flatpak_none_found": "Tiada ejen pengekodan. Secara lalai, kotak pasir Flatpak tidak dapat melihat ejen yang dipasang dalam folder rumah anda dan tiada akses rangkaian. Untuk membenarkan Claude Code dan penyesuai ACP-nya yang dipasang melalui nvm atau dalam ~/.local/bin, jalankan arahan ini dalam terminal, kemudian mulakan semula SolidCraft. Ia memberi akses kepada program dan fail log masuk yang disenaraikan, serta akses rangkaian:",
"ui.agent_chat_import_failed": "{name}: import gagal: {error}",
"ui.agent_chat_intro": "Sembang dengan ejen pengekodan biasa anda atau titik akhir model pilihan. Ejen mengubah reka bentuk melalui alat SolidCraft; perubahan dipaparkan terus dan boleh dibatalkan sekali bagi setiap mesej. Tetapkan kebenaran di Tetapan > AI.",
"ui.agent_chat_key_in_env": "kunci tersedia",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Pandangan sisi guna grid satah tegak yang menghadapnya, bukan satah tanah",
"ui.ground_shadow": "Bayang tanah",
"ui.groupSelected": "Kumpulkan Pilihan",
"ui.hatch": "Lorek",
"ui.heading_add_ins": "TAMBAHAN",
"ui.heading_assemble": "PASANG",
"ui.heading_box": "KOTAK",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "MIT-licentie",
"help.notice": "NOTICE",
"help.third_party": "Licenties van derden",
"home.back": "Terug naar {name}",
"home.crashes": "Crashrapporten",
"home.design_workspace": "Ontwerp",
"home.documentation": "Documentatie",
"home.export": "Exporteren",
"home.export_hint": "Kies een indeling voor het huidige ontwerp.",
"home.export_json_hint": "Bewerkbaar ontwerp en opdrachtgeschiedenis",
"home.export_mesh_hint": "Mesh om af te drukken of te delen",
"home.export_step_hint": "Nauwkeurige volumegeometrie",
"home.import": "Importeren…",
"home.import_hint": "STEP, IGES of een mesh in een nieuw ontwerp",
"home.in_this_browser": "in deze browser",
"home.missing": "ontbreekt",
"home.month_01": "jan.",
"home.month_02": "feb.",
"home.month_03": "mrt.",
"home.month_04": "apr.",
"home.month_05": "mei",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "aug.",
"home.month_09": "sep.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Nieuw ontwerp in inches",
"home.new_inches_hint": "Inches · opent in Ontwerp",
"home.new_mm": "Nieuw ontwerp",
"home.new_mm_hint": "Millimeters · opent in Ontwerp",
"home.open": "Openen…",
"home.opened_days_ago": "{count} dagen geleden geopend",
"home.opened_hours_ago": "{count} u geleden geopend",
"home.opened_just_now": "Zojuist geopend",
"home.opened_minutes_ago": "{count} min geleden geopend",
"home.open_hint": "Een .solidcraft-ontwerp ({shortcut})",
"home.open_now": "Momenteel geopend",
"home.opened_days_ago": "{count} dagen geleden",
"home.opened_hours_ago": "{count} u geleden",
"home.opened_just_now": "Zojuist",
"home.opened_minutes_ago": "{count} min geleden",
"home.opening": "openen…",
"home.properties": "Ontwerpeigenschappen",
"home.recent": "Recent",
"home.recent_empty": "Ontwerpen die u opent of opslaat, verschijnen hier.",
"home.recovery_count": "Herstelbare ontwerpen: {count}",
"home.recovery_empty": "Geen herstelbare ontwerpen.",
"home.recovery_hint": "SolidCraft is gesloten voordat deze ontwerpen werden opgeslagen.",
"home.report_issue": "Probleem melden",
"home.samples": "Voorbeelden",
"home.settings": "Instellingen",
"home.short_date": "{day} {month}",
"home.shortcuts": "Sneltoetsen",
"home.start_hint": "Maak een ontwerp, open een recent bestand of bekijk een voorbeeld.",
"home.whats_new": "Wat is er nieuw",
"home.yesterday": "Gisteren",
"install.button_tip": "Installeer SolidCraft als app: deze opent in een eigen venster en werkt offline",
"install.dock_1": "Kies Bestand › Toevoegen aan Dock.",
"install.dock_2": "Klik op Voeg toe.",
@@ -1021,7 +1059,7 @@
"search.try": "Typ een opdrachtnaam, een kenmerk in dit ontwerp of een parameter. Om een opdracht met waarden uit te voeren, typt u de id en een JSON-object: {example}",
"search.valid": "Geldig",
"settings.display_quality_note": "Fijnere niveaus tekenen gebogen vlakken en randen vloeiender en kosten meer tijd om te meshen. Alleen de viewport verandert, nooit het ontwerp of de exports.",
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave. Zolang dit aan staat, wordt de weergave continu opnieuw getekend.",
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave.",
"settings.measure_axis_distances": "X/Y/Z-afstanden tonen",
"settings.measure_axis_distances_help": "Toon de componentmaten langs de wereldassen naast de totale afstand terwijl Meten open is.",
"settings.msaa_applies_on_restart": "Anti-aliasing wordt toegepast bij de volgende start van SolidCraft",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Maximaal {max} bestanden per bericht; weggelaten: {files}",
"ui.agent_chat_files_loading": "Wachten tot de bestanden zijn gelezen",
"ui.agent_chat_files_max": "Maximaal {max} bestanden per bericht",
"ui.agent_chat_flatpak_none_found": "Geen codeeragent gevonden. De Flatpak-sandbox ziet standaard geen agents die in je persoonlijke map zijn geïnstalleerd en heeft geen netwerktoegang. Voer dit commando uit in een terminal en start SolidCraft daarna opnieuw om Claude Code en de ACP-adapter toe te staan die via nvm of in ~/.local/bin zijn geïnstalleerd. Het geeft toegang tot de genoemde programma’s en aanmeldbestanden, plus netwerktoegang:",
"ui.agent_chat_import_failed": "{name}: importeren mislukt: {error}",
"ui.agent_chat_intro": "Chat met een codeeragent die u al gebruikt, of met een modeleindpunt naar keuze. De agent werkt aan het geopende ontwerp via de tools van SolidCraft: de wijzigingen verschijnen terwijl ze worden gemaakt en worden per bericht als één stap ongedaan gemaakt. Wat hij zonder te vragen mag doen, stelt u in via Voorkeuren > AI.",
"ui.agent_chat_key_in_env": "sleutel ingesteld",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Zijaanzichten tonen het raster op het rechtopstaande vlak dat naar hen toe gericht is in plaats van op de grond",
"ui.ground_shadow": "Grondschaduw",
"ui.groupSelected": "Selectie groeperen",
"ui.hatch": "Arcering",
"ui.heading_add_ins": "INVOEGMODULES",
"ui.heading_assemble": "SAMENSTELLEN",
"ui.heading_box": "KUB",
+261 -5
View File
@@ -28,6 +28,7 @@
"Also deletes": "Usuwa również",
"Angle 2": "Kąt 2",
"Animate": "Animuj",
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
"Appearance name": "Nazwa wyglądu",
"Applied Radius": "Zastosowany promień",
"Apply To": "Zastosuj do",
@@ -83,6 +84,7 @@
"Contact": "Kontakt",
"Contact Sets": "Zestawy kontaktów",
"Contact sets": "Zestawy kontaktów",
"Continue on GitHub": "Kontynuuj na GitHubie",
"Continuity": "Ciągłość",
"Copy Error Message": "Kopiuj komunikat błędu",
"Copy Warning Message": "Kopiuj komunikat ostrzeżenia",
@@ -102,6 +104,7 @@
"Curv. Comb": "Grzebień krzyw.",
"Curvature (G2)": "Krzywizna (G2)",
"Curvature change (G3)": "Zmiana krzywizny (G3)",
"Custom": "Niestandardowy",
"Customize Toolbar": "Dostosuj pasek narzędzi",
"Delete": "Usuń",
"Delete all": "Usuń wszystko",
@@ -110,6 +113,8 @@
"Delete the set": "Usuń zestaw",
"Density": "Gęstość",
"Dependents": "Elementy zależne",
"Describe the problem you saw": "Opisz zauważony problem",
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
"Direction": "Kierunek",
@@ -127,6 +132,7 @@
"Enabled": "Włączone",
"End bulge": "Wybrzuszenie końca",
"End continuity": "Ciągłość końca",
"Expected result (optional)": "Oczekiwany wynik (opcjonalnie)",
"Explode": "Rozstrzel",
"Explode Text": "Rozbij tekst",
"Export Animation…": "Eksportuj animację…",
@@ -168,6 +174,7 @@
"Height": "Wysokość",
"Hidden edges": "Krawędzie niewidoczne",
"History layout": "Układ historii",
"History names": "Nazwy w historii",
"Holes": "Otwory",
"Holes (edges on them)": "Otwory (krawędzie na nich)",
"Home view": "Widok główny",
@@ -234,6 +241,7 @@
"No matching appearance presets.": "Brak pasujących ustawień wyglądu.",
"No matching appearances": "Brak pasujących wyglądów",
"No visible bodies or sketches to export.": "Brak widocznych brył lub szkiców do wyeksportowania.",
"Nominal diameter": "Średnica nominalna",
"None": "Brak",
"Not available yet": "Jeszcze niedostępne",
"Notch": "Wycięcie",
@@ -242,7 +250,10 @@
"Open GitHub draft": "Otwórz szkic zgłoszenia na GitHubie",
"Open local libraries in the desktop app. You can export this design's library here.": "Otwórz biblioteki lokalne w aplikacji komputerowej. Tutaj możesz wyeksportować bibliotekę tego projektu.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Otwartoźródłowy parametryczny CAD 3D w języku Rust. Licencja MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Otwiera przeglądarkę internetową i kopiuje treść raportu, którą wkleisz na GitHubie.",
"Opens your browser. Review and submit your report on GitHub.": "Otwiera przeglądarkę internetową. Sprawdź raport i wyślij go na GitHubie.",
"Operator": "Operator",
"Orbit (right drag)": "Obracanie widoku (przeciąganie prawym przyciskiem)",
"Other side": "Druga strona",
"Other surface (mutual trim)": "Druga powierzchnia (wzajemne przycięcie)",
"Outside": "Na zewnątrz",
@@ -257,6 +268,7 @@
"Pick the tool bodies": "Wybierz bryły narzędziowe",
"Picked": "Wybrane",
"Picked point": "Wybrany punkt",
"Pitch": "Skok",
"Place at the origin instead": "Zamiast tego umieść w początku układu",
"Plastic Rules": "Reguły tworzyw",
"Playback": "Odtwarzanie",
@@ -290,6 +302,8 @@
"Remove the track": "Usuń ścieżkę",
"Remove this point": "Usuń ten punkt",
"Report a bug — review a screenshot and open a GitHub draft": "Zgłoś błąd — sprawdź zrzut ekranu i otwórz szkic zgłoszenia na GitHubie",
"Report text copied. Paste it into GitHub and submit your report there.": "Skopiowano treść raportu. Wklej ją na GitHubie i tam wyślij raport.",
"Reports are public. A GitHub account is required.": "Raporty są publiczne. Wymagane jest konto GitHub.",
"Reset Toolbar": "Przywróć pasek narzędzi",
"Resize Part": "Zmień rozmiar części",
"Restore View": "Przywróć widok",
@@ -304,6 +318,7 @@
"Revert Pos.": "Przywróć poł.",
"Revert Position": "Przywróć położenie",
"Review the report and screenshot before opening GitHub.": "Przed otwarciem GitHuba sprawdź raport i zrzut ekranu.",
"Review your report on GitHub and click Submit new issue to publish it.": "Sprawdź raport na GitHubie i kliknij Submit new issue, aby go opublikować.",
"Rim Edge": "Krawędź obrzeża",
"Roll History Marker Here": "Przesuń tutaj znacznik historii",
"Round": "Okrągłe",
@@ -314,10 +329,12 @@
"SURFACE QUALITY": "JAKOŚĆ POWIERZCHNI",
"Save Rule": "Zapisz regułę",
"Save View": "Zapisz widok",
"Save screenshot…": "Zapisz zrzut ekranu…",
"Save the study as an animated GIF, seen as the viewport shows it": "Zapisz badanie jako animowany GIF, tak jak widać je w widoku",
"Saved preview. Geometry is being checked before editing.": "Zapisany podgląd. Geometria jest sprawdzana przed edycją.",
"Scale factor": "Współczynnik skali",
"Scene": "Scena",
"Screenshot (optional)": "Zrzut ekranu (opcjonalnie)",
"Search": "Szukaj",
"Search commands": "Szukaj poleceń",
"Search this library": "Przeszukaj tę bibliotekę",
@@ -352,17 +369,21 @@
"Start continuity": "Ciągłość początku",
"Step": "Krok",
"Steps": "Kroki",
"Steps to reproduce (optional)": "Kroki do odtworzenia (opcjonalnie)",
"Stitch the kept pieces": "Zszyj zachowane części",
"Stitch to both bodies": "Zszyj z obiema bryłami",
"Study": "Badanie",
"Style": "Styl",
"Summary": "Podsumowanie",
"Suppressed": "Wygaszone",
"Surface": "Powierzchnia",
"Swap Ends": "Zamień końce",
"TRIM": "PRZYCINANIE",
"Tangent (G1)": "Styczność (G1)",
"Tangent Chain": "Łańcuch styczny",
"Tap drill diameter": "Średnica wiertła pod gwint",
"Tear": "Rozdarcie",
"Technical details": "Szczegóły techniczne",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Przerywany szkic to zapisana płaszczyzna pomocnicza. Wybierz nową ścianę, aby naprawić zaczepienie.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Brak zapisanego wystąpienia autora. Przed ponownym powiązaniem wybierz prawidłowe położenie autora.",
"Then": "Wtedy",
@@ -374,6 +395,7 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Ten element podrzędny jest współdzielony. Usunięcie go usuwa go z każdej kopii elementu nadrzędnego.",
"This child is shared. Position changes apply to every copy of its parent.": "Ten element podrzędny jest współdzielony. Zmiany położenia dotyczą każdej kopii elementu nadrzędnego.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ta ściana nie zapewnia trwałego zaczepienia. Wybierz inną ścianę lub jawnie użyj stałej płaszczyzny.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Ten obraz nie jest jeszcze dołączony na GitHubie. Skopiuj go lub zapisz, a potem dołącz na GitHubie.",
"To Offset": "Do odsunięcia",
"To edges": "Do krawędzi",
"Tolerance": "Tolerancja",
@@ -399,6 +421,9 @@
"Wall": "Ścianka",
"Wall Location": "Położenie ścianki",
"Wall Thickness": "Grubość ścianki",
"What did you do just before the problem? A short list is enough": "Co robiłeś tuż przed wystąpieniem problemu? Wystarczy krótka lista",
"What did you expect to see?": "Czego się spodziewałeś?",
"What happened?": "Co się stało?",
"When": "Gdy",
"Width": "Szerokość",
"Will fail": "Nie powiedzie się",
@@ -409,6 +434,29 @@
"Z angle": "Kąt Z",
"Z distance": "Odległość Z",
"Zebra": "Zebra",
"ai.prefs.availability": "Sterowanie asystentem",
"ai.prefs.background_commands": "Uruchamiaj polecenia agenta w tle",
"ai.prefs.follow_camera": "Obracaj kamerę do szkiców",
"ai.prefs.follow_camera_off": "Automatyczne podążanie kamery jest wyłączone.",
"ai.prefs.follow_camera_on": "Kamera obraca się przodem do szkiców tworzonych przez agenta.",
"ai.prefs.intro": "Agenty, uprawnienia i sposób pokazywania ich pracy.",
"ai.prefs.make_default": "Używaj domyślnie",
"ai.prefs.no_agents": "Brak agentów. Dodaj agenta lub odśwież, aby wyszukać zainstalowane agenty.",
"ai.prefs.no_saved_approvals": "Brak zapisanych zgód.",
"ai.prefs.override_all_warning": "Ten agent może zapisywać, nadpisywać, eksportować i zastępować projekt bez pytania.",
"ai.prefs.permission_all": "Zezwalaj na wszystko",
"ai.prefs.permission_all_summary": "Dostępne narzędzia SolidCraft działają bez pytania.",
"ai.prefs.permission_ask": "Pytaj przed zmianami",
"ai.prefs.permission_ask_summary": "Zatwierdzaj każdą zmianę projektu i każdą operację na plikach.",
"ai.prefs.permission_edits": "Zezwalaj na edycję modelu",
"ai.prefs.permission_edits_summary": "Edycja modelu działa bez pytania; operacje na plikach i nieodwracalne nadal wymagają pytania.",
"ai.prefs.permission_scope": "Dotyczy tylko narzędzi SolidCraft. Powłoka agenta i edycja plików podlegają jego własnym uprawnieniom.",
"ai.prefs.pointer_preview_hidden": "Wskaźnik ukryty",
"ai.prefs.pointer_preview_speed": "Wskaźnik agenta · {speed}",
"ai.prefs.pointer_speed": "Szybkość ruchu",
"ai.prefs.privacy_note": "Rozmowę otrzymuje tylko wybrany przez Ciebie agent lub punkt końcowy. Klucze punktów końcowych są przechowywane w systemowym pęku kluczy.",
"ai.prefs.show_pointer": "Pokaż wskaźnik agenta",
"ai.prefs.turn_limit_hint": "Maksymalna liczba wywołań narzędzi zmieniających projekt, które asystent może wykonać dla jednej wiadomości.",
"angle, height": "kąt, wysokość",
"appearance.dielectric_ior": "Współczynnik załamania dielektryka (IOR)",
"appearance.metallic": "Metaliczność",
@@ -759,6 +807,7 @@
"cmd.view_graphics_preset": "Ustawienie graficzne",
"cmd.view_list": "Lista widoków nazwanych",
"cmd.view_navigate": "Nawiguj widokiem",
"cmd.view_normal_to": "Prostopadle do zaznaczenia",
"cmd.view_rename": "Zmień nazwę widoku nazwanego",
"cmd.view_save": "Nowy widok nazwany",
"color.copy": "Kliknij, aby skopiować wartości koloru",
@@ -856,20 +905,64 @@
"help.licences": "Licencje",
"help.mit": "Licencja MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Ścina zaznaczone krawędzie płaską fazą o zadanej odległości. Służy do łamania ostrych krawędzi lub ułatwienia wprowadzenia kołka czy śruby.",
"help.solid.extrude": "Wyciąga profil szkicu w 3D na zadaną odległość, dodając lub usuwając materiał. Służy do zamiany płaskiego obrysu w blok, występ lub kieszeń.",
"help.solid.fillet": "Zaokrągla zaznaczone krawędzie gładkim łukiem o zadanym promieniu. Służy do złagodzenia ostrych krawędzi dla wytrzymałości, wygody lub wyglądu.",
"help.solid.hole": "Wierci okrągły otwór o zadanej średnicy w wybranym punkcie ściany. Służy do otworów przelotowych pod śruby, kołki i innych prostych okrągłych otworów.",
"help.solid.revolve": "Obraca profil szkicu wokół osi, tworząc okrągłą bryłę. Służy do wałków, gałek, misek i wszystkich części okrągłych względem osi.",
"help.solid.shell": "Wydrąża bryłę, pozostawiając ścianki o zadanej grubości, i może otworzyć wybrane ściany. Służy do pudełek, pokryw i innych części cienkościennych.",
"help.third_party": "Licencje stron trzecich",
"home.back": "Wróć do {name}",
"home.crashes": "Raporty awarii",
"home.design_workspace": "Projektowanie",
"home.documentation": "Dokumentacja",
"home.export": "Eksportuj",
"home.export_hint": "Wybierz format dla bieżącego projektu.",
"home.export_json_hint": "Edytowalny projekt i historia poleceń",
"home.export_mesh_hint": "Siatka do druku lub udostępniania",
"home.export_step_hint": "Precyzyjna geometria bryłowa",
"home.import": "Importuj…",
"home.import_hint": "STEP, IGES lub siatka do nowego projektu",
"home.in_this_browser": "w tej przeglądarce",
"home.missing": "brak",
"home.month_01": "sty",
"home.month_02": "lut",
"home.month_03": "mar",
"home.month_04": "kwi",
"home.month_05": "maj",
"home.month_06": "cze",
"home.month_07": "lip",
"home.month_08": "sie",
"home.month_09": "wrz",
"home.month_10": "paź",
"home.month_11": "lis",
"home.month_12": "gru",
"home.new_inches": "Nowy projekt w calach",
"home.new_inches_hint": "Cale · otwiera w obszarze Projektowanie",
"home.new_mm": "Nowy projekt",
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
"home.open": "Otwórz…",
"home.opened_days_ago": "Otwarto {count} dni temu",
"home.opened_hours_ago": "Otwarto {count} godz. temu",
"home.opened_just_now": "Otwarto przed chwilą",
"home.opened_minutes_ago": "Otwarto {count} min temu",
"home.open_hint": "Projekt .solidcraft ({shortcut})",
"home.open_now": "Otwórz teraz",
"home.opened_days_ago": "{count} dni temu",
"home.opened_hours_ago": "{count} godz. temu",
"home.opened_just_now": "Przed chwilą",
"home.opened_minutes_ago": "{count} min temu",
"home.opening": "otwieranie…",
"home.properties": "Właściwości projektu",
"home.recent": "Ostatnie",
"home.recent_empty": "Tutaj pojawią się projekty, które otworzysz lub zapiszesz.",
"home.recovery_count": "Projekty do odzyskania: {count}",
"home.recovery_empty": "Brak projektów do odzyskania.",
"home.recovery_hint": "SolidCraft zamknął się przed zapisaniem tych projektów.",
"home.report_issue": "Zgłoś problem",
"home.samples": "Przykłady",
"home.settings": "Ustawienia",
"home.short_date": "{day} {month}",
"home.shortcuts": "Skróty klawiszowe",
"home.start_hint": "Utwórz projekt, otwórz ostatni plik lub obejrzyj przykład.",
"home.whats_new": "Co nowego",
"home.yesterday": "Wczoraj",
"install.button_tip": "Zainstaluj SolidCraft jako aplikację: otwiera się we własnym oknie i działa offline",
"install.dock_1": "Wybierz Plik › Dodaj do Docka.",
"install.dock_2": "Kliknij Dodaj.",
@@ -927,6 +1020,8 @@
"motion.to_end": "Na koniec",
"motion.to_start": "Na początek",
"motion.track_name": "Ścieżka {number}",
"nav.free_orbit.help": "Mysz i mysz 3D obracają widok przez oba bieguny i zachowują przechylenie. Przyciąganie do osi z Shift jest wyłączone.",
"nav.free_orbit.label": "Swobodne obracanie",
"nav.touchpad.help": "Przytrzymaj Shift i poruszaj wskaźnikiem, aby przesuwać; przytrzymaj Alt/Option i poruszaj nim, aby obracać widok. Przycisk myszy nie jest potrzebny. Przewijanie w pionie powiększa.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Obracanie widoku (przytrzymaj Alt/Option i poruszaj; bez przycisku myszy)",
@@ -949,8 +1044,130 @@
"pattern.flip_direction": "Odwróć kierunek",
"pick a hole to seat it, or OK places it at the origin": "wybierz otwór, aby go osadzić, lub OK umieszcza go w początku układu",
"pick one or two vertices, edges or faces": "wybierz jeden lub dwa wierzchołki, krawędzie lub ściany",
"prefs.add_item": "Dodaj element",
"prefs.alt_frees_manipulators": "Alt / Option zwalnia również strzałki manipulatora",
"prefs.anti_aliasing_note": "Wygładza poszarpane krawędzie.",
"prefs.applies_on_restart": "Działa po ponownym uruchomieniu",
"prefs.assistant": "Asystent",
"prefs.axes": "Osie",
"prefs.axis": "Oś",
"prefs.bottom": "Dół",
"prefs.browser": "Przeglądarka",
"prefs.built_in": "Wbudowana",
"prefs.button_left": "Przeciąganie lewym",
"prefs.button_middle": "Przeciąganie środkowym",
"prefs.button_right": "Przeciąganie prawym",
"prefs.choose_font": "Wybierz plik czcionki…",
"prefs.click": "Kliknięcie",
"prefs.contact_shading": "Miękkie cieniowanie kontaktowe",
"prefs.contact_shading_note": "Miękkie cieniowanie tam, gdzie części są blisko siebie.",
"prefs.context_menu": "Menu kontekstowe",
"prefs.context_menu_ring": "Menu kontekstowe · przytrzymaj, aby otworzyć menu promieniste",
"prefs.cpu": "CPU (programowo)",
"prefs.crash_reports": "Otwórz raporty o awariach…",
"prefs.crash_reports_note": "Raporty są przechowywane na tym komputerze, razem z projektem i poleceniami, które odtwarzają problem.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Powierzchnie zakrzywione",
"prefs.curves": "Powierzchnie zakrzywione",
"prefs.dead_zone_note": "Zakreskowany pas to strefa martwa: ruch wewnątrz niej jest pomijany.",
"prefs.default": "domyślnie",
"prefs.design": "Ustawienia domyślne projektu",
"prefs.design_note": "Ustawienia, z którymi startują nowe projekty. Otwarte projekty zachowują własne.",
"prefs.devices": "Mysz 3D",
"prefs.display": "Wyświetlanie",
"prefs.edges": "Krawędzie i cieniowanie",
"prefs.extend": "Rozszerz",
"prefs.extra_navigation": "Ctrl+przeciąganie lewym na pustym miejscu obraca widok; przytrzymaj Spację, aby przesuwać.",
"prefs.follows_os": "Zgodnie z systemem",
"prefs.font": "Czcionka",
"prefs.font_fallback": "Znaki, których brakuje w wybranej czcionce, są brane z czcionki wbudowanej, więc żadna etykieta nie pokaże pustego prostokąta.",
"prefs.font_sample": "Zażółć gęślą jaźń · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Nie można odczytać wybranej czcionki.",
"prefs.general": "Ogólne",
"prefs.general_note": "Język, zapisywanie i aktualizacje.",
"prefs.gesture_orbit": "Obracanie widoku",
"prefs.gesture_pan": "Przesuwanie",
"prefs.gesture_zoom": "Powiększanie",
"prefs.graphics": "Grafika",
"prefs.graphics_note": "Wybierz między szybkością a gładkością. Każda opcja pokazuje swój efekt na przykładzie.",
"prefs.graphics_presets_note": "Ustawienia wstępne zmieniają razem poniższe opcje.",
"prefs.history": "Historia",
"prefs.hold_stop_snapping": "Przytrzymaj, aby wyłączyć przyciąganie",
"prefs.icons_only": "Tylko ikony",
"prefs.imported_file": "Zaimportowany plik",
"prefs.input": "Wprowadzanie",
"prefs.interface": "Interfejs",
"prefs.interface_language": "Język interfejsu",
"prefs.interface_note": "Wygląd SolidCraft i rozmieszczenie jego elementów.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Etykiety",
"prefs.layout": "Układ",
"prefs.layout_note": "Kompaktowy dokuje panele do krawędzi okna. Przezroczystość przeglądarki jest dostępna w układzie Pływający. Stronę asystenta zmienisz na stronie Asystent.",
"prefs.left": "Lewo",
"prefs.left_click": "Kliknięcie lewym",
"prefs.live_input": "Bieżące wejście",
"prefs.live_preview": "Podgląd na żywo",
"prefs.minutes": "minut",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mysz i gładzik",
"prefs.mouse_heading": "Mysz",
"prefs.mouse_mapping": "Przypisanie przycisków myszy",
"prefs.navigation": "Nawigacja",
"prefs.navigation_note": "Jak mysz, gładzik i mysz 3D poruszają widokiem.",
"prefs.not_on_gpu": "niedostępne na tym GPU",
"prefs.on": "Włączone",
"prefs.opaque": "Nieprzezroczysta",
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
"prefs.orientation": "Orientacja",
"prefs.overall_speed": "Szybkość ogólna",
"prefs.preview_choices": "Podgląd wybranych opcji",
"prefs.quality_default": "domyślnie",
"prefs.quality_fastest": "najszybciej",
"prefs.quality_slowest": "najwolniej",
"prefs.quality_smoother": "gładziej",
"prefs.reports_saved": "Zapisane: {n}",
"prefs.right_click": "Kliknięcie prawym",
"prefs.saving": "Autozapis i odzyskiwanie",
"prefs.scale_note": "Także Ctrl + / Ctrl − w oknie. Przyciski mają 28 px przy 100%.",
"prefs.scale_text": "Skala i tekst",
"prefs.select": "Zaznacz",
"prefs.select_one_item": "Zaznacz jeden element",
"prefs.selecting": "Zaznaczanie",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift tylko dodaje; Ctrl / Cmd przełącza",
"prefs.shift_add_note": "Włączone: Shift dodaje, Ctrl / Cmd przełącza. Wyłączone (domyślnie): oba przełączają.",
"prefs.shortcuts": "Skróty",
"prefs.sketch_selection": "Szkic i zaznaczanie",
"prefs.sketch_selection_heading": "Szkic i zaznaczanie",
"prefs.sketch_selection_note": "Jak działa przyciąganie w szkicach i jak kliknięcia zaznaczają.",
"prefs.sketching": "Szkicowanie",
"prefs.snap_drawing_note": "Podczas rysowania lub przeciągania punktu szkicu.",
"prefs.source": "Źródło",
"prefs.speed": "Szybkość",
"prefs.stand_up_note": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X.",
"prefs.status_done": "Gotowe",
"prefs.status_error": "Błąd",
"prefs.status_selected": "Zaznaczone",
"prefs.status_warning": "Ostrzeżenie",
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
"prefs.theme_file": "plik motywu",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "teraz",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Dodaj element lub usuń go ponownie",
"prefs.toolbar": "Pasek narzędzi",
"prefs.translucent": "Półprzezroczysta",
"prefs.under_browser": "Pod przeglądarką",
"prefs.under_cursor": "Pod kursorem",
"prefs.units_precision": "Jednostki i dokładność",
"prefs.updates": "Aktualizacje",
"prefs.view": "Zachowanie widoku",
"prefs.viewport": "Widok",
"prefs.wheel": "Kółko",
"prefs.wheel_trackpad": "Kółko i gładzik",
"prefs.workspace": "Środowisko pracy",
"prefs.zoom_toward_cursor": "Powiększanie w stronę kursora",
"properties.title": "Właściwości: {name}",
"recovery.days_ago": "{count} dni temu",
"recovery.detail": "{file} · elementy: {features} · autozapis {when}",
@@ -1022,7 +1239,7 @@
"search.try": "Wpisz nazwę polecenia, element tego projektu lub parametr. Aby uruchomić polecenie z wartościami, wpisz jego identyfikator i obiekt JSON: {example}",
"search.valid": "Prawidłowe",
"settings.display_quality_note": "Wyższe poziomy rysują zakrzywione ściany i krawędzie gładziej, ale siatka powstaje dłużej. Zmienia się tylko widok, nigdy projekt ani eksport.",
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku. Gdy jest włączone, widok odświeża się bez przerwy.",
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku.",
"settings.measure_axis_distances": "Pokaż odległości X/Y/Z",
"settings.measure_axis_distances_help": "Gdy pomiar jest otwarty, pokazuj składowe wzdłuż osi globalnych obok odległości całkowitej.",
"settings.msaa_applies_on_restart": "Wygładzanie krawędzi zadziała po ponownym uruchomieniu SolidCraft",
@@ -1067,6 +1284,9 @@
"thread.designation": "Oznaczenie",
"thread.iso_metric": "Metryczny ISO",
"thread.select_size": "Wybierz rozmiar",
"timeline.delete_after_marker_one": "Usuń 1 funkcję za znacznikiem historii",
"timeline.delete_after_marker_other": "Usuń funkcje za znacznikiem historii: {count}",
"timeline.delete_many": "Usuń zaznaczone funkcje: {count}",
"timeline.group": "Nowy folder funkcji",
"timeline.move_many": "Przenieś elementy osi czasu",
"timeline.move_to_group": "Przenieś funkcje do folderu",
@@ -1145,6 +1365,7 @@
"ui.agent_chat_files_left_out": "Do jednej wiadomości można dołączyć najwyżej {max} plików; pominięto: {files}",
"ui.agent_chat_files_loading": "Oczekiwanie na odczytanie plików",
"ui.agent_chat_files_max": "Najwyżej {max} plików w jednej wiadomości",
"ui.agent_chat_flatpak_none_found": "Nie znaleziono agenta programistycznego. Piaskownica Flatpak domyślnie nie widzi agentów zainstalowanych w folderze domowym i nie ma dostępu do sieci. Aby zezwolić na Claude Code i jego adapter ACP zainstalowane przez nvm lub w ~/.local/bin, uruchom to polecenie w terminalu, a potem uruchom ponownie SolidCraft. Daje ono dostęp do wymienionych programów i plików logowania oraz dostęp do sieci:",
"ui.agent_chat_import_failed": "{name}: import się nie powiódł: {error}",
"ui.agent_chat_intro": "Rozmawiaj z agentem programistycznym, którego już używasz, lub z wybranym punktem końcowym modelu. Pracuje on na otwartym projekcie za pomocą narzędzi SolidCraft: jego zmiany są widoczne na bieżąco, a cofa się je jednym krokiem na wiadomość. To, co może robić bez pytania, ustawisz w Preferencje > AI.",
"ui.agent_chat_key_in_env": "klucz ustawiony",
@@ -1212,6 +1433,8 @@
"ui.agent_chat_waiting": "Czeka na Ciebie",
"ui.agent_cursor": "Kursor agenta",
"ui.agent_cursor_speed": "Szybkość kursora agenta",
"ui.agent_off_thread": "Zachowaj responsywność okna, gdy agenty wykonują polecenia",
"ui.agent_off_thread_note": "Wolne polecenia agenta (z panelu AI lub klienta MCP) są liczone obok okna: możesz dalej obracać i oglądać widok oraz zatrzymać polecenie przyciskiem w widoku. Wyłączone: każde działa w wątku okna, a okno na nie czeka.",
"ui.ai_add": "Dodaj",
"ui.ai_add_agent": "Dodaj agenta…",
"ui.ai_add_agent_title": "Dodaj agenta",
@@ -1464,6 +1687,7 @@
"ui.grid_follows_view_hint": "Widoki boczne pokazują siatkę na pionowej płaszczyźnie zwróconej do nich zamiast na podłożu",
"ui.ground_shadow": "Cień na podłożu",
"ui.groupSelected": "Grupuj zaznaczone",
"ui.hatch": "Kreskowanie",
"ui.heading_add_ins": "DODATKI",
"ui.heading_assemble": "ZŁOŻENIE",
"ui.heading_box": "PROSTOPADŁOŚCIAN",
@@ -1539,10 +1763,17 @@
"ui.hideProfile": "Ukryj profil",
"ui.history_back": "Cofnij o jeden krok",
"ui.history_below_browser": "Pod przeglądarką",
"ui.history_edge_hint": "kliknij dwukrotnie krawędź",
"ui.history_first": "Przejdź do początku",
"ui.history_forward": "Przejdź o jeden krok do przodu",
"ui.history_hide_names": "Ukryj nazwy",
"ui.history_last": "Przejdź do końca",
"ui.history_marker_tip": "Znacznik historii: przeciągnij, aby cofnąć model; nowe funkcje trafiają tutaj",
"ui.history_names": "Nazwy",
"ui.history_play": "Odtwórz do końca",
"ui.history_show_names": "Pokaż nazwy",
"ui.history_title": "Historia",
"ui.history_toggle_names": "Pokaż lub ukryj nazwy funkcji",
"ui.hole": "Otwór",
"ui.hole_tap_type": "Typ gwintu otworu",
"ui.hole_type": "Typ otworu",
@@ -1754,6 +1985,7 @@
"ui.smooth_wheel_zoom": "Płynne powiększanie kółkiem",
"ui.smooth_wheel_zoom_tip": "Wyłącz, aby każdy ruch kółka działał od razu, bez wygładzania",
"ui.snap": "Przyciąganie",
"ui.space_mouse_axes_reassigned": "Osie są przypisane inaczej w Preferencjach i to przypisanie nadal działa niezależnie od tej kalibracji.",
"ui.space_mouse_calibrate": "Kalibruj…",
"ui.space_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
"ui.space_mouse_calibrate_title": "Kalibracja myszy 3D",
@@ -1763,12 +1995,26 @@
"ui.space_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
"ui.space_mouse_dead_zone": "Strefa martwa",
"ui.space_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
"ui.space_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
"ui.space_mouse_driven_by_label": "sterowana przez",
"ui.space_mouse_driver": "Sterownik myszy 3D",
"ui.space_mouse_driver_auto": "Automatycznie",
"ui.space_mouse_driver_direct": "Bezpośrednio (hidraw)",
"ui.space_mouse_driver_only_direct": "Bezpośredni odczyt urządzenia; spacenavd nie działa",
"ui.space_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyny dostępny sposób",
"ui.space_mouse_forget": "Zapomnij kalibrację",
"ui.space_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
"ui.space_mouse_motion_rotation_x": "Obrót X",
"ui.space_mouse_motion_rotation_y": "Obrót Y",
"ui.space_mouse_motion_rotation_z": "Obrót Z",
"ui.space_mouse_motion_translation_x": "Przesunięcie X",
"ui.space_mouse_motion_translation_y": "Przesunięcie Y",
"ui.space_mouse_motion_translation_z": "Przesunięcie Z",
"ui.space_mouse_navigate": "Nawiguj za pomocą SpaceNavigator / SpaceMouse",
"ui.space_mouse_none": "Nie znaleziono myszy 3D",
"ui.space_mouse_not_calibrated": "Nieskalibrowano",
"ui.space_mouse_reading": "Odczyt",
"ui.space_mouse_reset_axes": "Resetuj przypisanie osi",
"ui.space_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
@@ -1782,6 +2028,16 @@
"ui.space_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
"ui.space_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.space_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
"ui.space_mouse_view_axis": "Oś widoku",
"ui.space_mouse_view_off": "Wyłączona",
"ui.space_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tutaj ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
"ui.space_mouse_view_orbit_sideways": "Obracanie w lewo / w prawo",
"ui.space_mouse_view_orbit_up_down": "Obracanie w górę / w dół",
"ui.space_mouse_view_pan_sideways": "Przesuwanie w lewo / w prawo",
"ui.space_mouse_view_pan_up_down": "Przesuwanie w górę / w dół",
"ui.space_mouse_view_roll": "Przechylenie",
"ui.space_mouse_view_unmapped": "Oś {n}",
"ui.space_mouse_view_zoom": "Powiększanie",
"ui.spacing": "Odstęp",
"ui.spacing_2": "Odstęp 2",
"ui.sphere": "Sfera",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Licença MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenças de terceiros",
"home.back": "Voltar para {name}",
"home.crashes": "Relatórios de falhas",
"home.design_workspace": "Projeto",
"home.documentation": "Documentação",
"home.export": "Exportar",
"home.export_hint": "Escolha um formato para o projeto atual.",
"home.export_json_hint": "Projeto editável e histórico de comandos",
"home.export_mesh_hint": "Malha para impressão ou compartilhamento",
"home.export_step_hint": "Geometria sólida precisa",
"home.import": "Importar…",
"home.import_hint": "STEP, IGES ou malha em um novo projeto",
"home.in_this_browser": "neste navegador",
"home.missing": "ausente",
"home.month_01": "jan.",
"home.month_02": "fev.",
"home.month_03": "mar.",
"home.month_04": "abr.",
"home.month_05": "mai.",
"home.month_06": "jun.",
"home.month_07": "jul.",
"home.month_08": "ago.",
"home.month_09": "set.",
"home.month_10": "out.",
"home.month_11": "nov.",
"home.month_12": "dez.",
"home.new_inches": "Novo projeto em polegadas",
"home.new_inches_hint": "Polegadas · abre em Projeto",
"home.new_mm": "Novo projeto",
"home.new_mm_hint": "Milímetros · abre em Projeto",
"home.open": "Abrir…",
"home.opened_days_ago": "Aberto há {count} dias",
"home.opened_hours_ago": "Aberto há {count} h",
"home.opened_just_now": "Aberto agora mesmo",
"home.opened_minutes_ago": "Aberto há {count} min",
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
"home.open_now": "Abrir agora",
"home.opened_days_ago": "Há {count} dias",
"home.opened_hours_ago": "Há {count} h",
"home.opened_just_now": "Agora mesmo",
"home.opened_minutes_ago": "Há {count} min",
"home.opening": "abrindo…",
"home.properties": "Propriedades do projeto",
"home.recent": "Recentes",
"home.recent_empty": "Os projetos que você abrir ou salvar aparecerão aqui.",
"home.recovery_count": "Projetos recuperáveis: {count}",
"home.recovery_empty": "Nenhum projeto recuperável.",
"home.recovery_hint": "O SolidCraft foi fechado antes que estes projetos fossem salvos.",
"home.report_issue": "Relatar um problema",
"home.samples": "Exemplos",
"home.settings": "Configurações",
"home.short_date": "{day} {month}",
"home.shortcuts": "Atalhos de teclado",
"home.start_hint": "Crie um projeto, abra um arquivo recente ou explore um exemplo.",
"home.whats_new": "Novidades",
"home.yesterday": "Ontem",
"install.button_tip": "Instale o SolidCraft como aplicativo: ele abre em sua própria janela e funciona offline",
"install.dock_1": "Escolha Arquivo › Adicionar ao Dock.",
"install.dock_2": "Clique em Adicionar.",
@@ -1022,7 +1060,7 @@
"search.try": "Digite o nome de um comando, um recurso deste projeto ou um parâmetro. Para executar um comando com valores, digite seu id e um objeto JSON: {example}",
"search.valid": "Válido",
"settings.display_quality_note": "Níveis mais finos desenham faces e arestas curvas mais suaves e demoram mais para gerar a malha. Só a janela de visualização muda, nunca o projeto ou as exportações.",
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista. Enquanto estiver ativo, a vista é redesenhada continuamente.",
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista.",
"settings.measure_axis_distances": "Mostrar distâncias X/Y/Z",
"settings.measure_axis_distances_help": "Mostrar as componentes nos eixos do mundo junto com a distância total enquanto Medir está aberto.",
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "No máximo {max} arquivos acompanham uma mensagem; deixados de fora: {files}",
"ui.agent_chat_files_loading": "Aguardando a leitura dos arquivos",
"ui.agent_chat_files_max": "No máximo {max} arquivos por mensagem",
"ui.agent_chat_flatpak_none_found": "Nenhum agente de programação encontrado. Por padrão, a sandbox do Flatpak não vê agentes instalados na sua pasta pessoal e não tem acesso à rede. Para permitir o Claude Code e o adaptador ACP dele instalados via nvm ou em ~/.local/bin, execute este comando em um terminal e reinicie o SolidCraft. Ele concede acesso aos programas e arquivos de login listados, além de acesso à rede:",
"ui.agent_chat_import_failed": "{name}: falha na importação: {error}",
"ui.agent_chat_intro": "Converse com um agente de programação que você já usa, ou com um endpoint de modelo que você escolher. Ele trabalha no projeto aberto pelas ferramentas do SolidCraft: as alterações aparecem conforme são feitas e se desfazem em um passo por mensagem. O que ele pode fazer sem perguntar é definido em Preferências > IA.",
"ui.agent_chat_key_in_env": "chave definida",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "As vistas laterais mostram a grade no plano vertical voltado para elas, em vez do solo",
"ui.ground_shadow": "Sombra no chão",
"ui.groupSelected": "Agrupar seleção",
"ui.hatch": "Hachura",
"ui.heading_add_ins": "COMPLEMENTOS",
"ui.heading_assemble": "MONTAR",
"ui.heading_box": "CAIXA",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "Licența MIT",
"help.notice": "NOTICE",
"help.third_party": "Licențe terțe",
"home.back": "Înapoi la {name}",
"home.crashes": "Rapoarte de blocare",
"home.design_workspace": "Proiectare",
"home.documentation": "Documentație",
"home.export": "Exportă",
"home.export_hint": "Alege un format pentru proiectul curent.",
"home.export_json_hint": "Proiect editabil și istoricul comenzilor",
"home.export_mesh_hint": "Plasă pentru imprimare sau partajare",
"home.export_step_hint": "Geometrie solidă precisă",
"home.import": "Importă…",
"home.import_hint": "STEP, IGES sau plasă într-un proiect nou",
"home.in_this_browser": "în acest browser",
"home.missing": "lipsă",
"home.month_01": "ian.",
"home.month_02": "feb.",
"home.month_03": "mar.",
"home.month_04": "apr.",
"home.month_05": "mai",
"home.month_06": "iun.",
"home.month_07": "iul.",
"home.month_08": "aug.",
"home.month_09": "sept.",
"home.month_10": "oct.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Proiect nou în inci",
"home.new_inches_hint": "Inci · se deschide în Proiectare",
"home.new_mm": "Proiect nou",
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
"home.open": "Deschide…",
"home.opened_days_ago": "Deschis acum {count} zile",
"home.opened_hours_ago": "Deschis acum {count} h",
"home.opened_just_now": "Deschis chiar acum",
"home.opened_minutes_ago": "Deschis acum {count} min",
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
"home.open_now": "Deschis acum",
"home.opened_days_ago": "Acum {count} zile",
"home.opened_hours_ago": "Acum {count} h",
"home.opened_just_now": "Chiar acum",
"home.opened_minutes_ago": "Acum {count} min",
"home.opening": "se deschide…",
"home.properties": "Proprietățile proiectului",
"home.recent": "Recente",
"home.recent_empty": "Proiectele pe care le deschizi sau le salvezi apar aici.",
"home.recovery_count": "Proiecte recuperabile: {count}",
"home.recovery_empty": "Nu există proiecte recuperabile.",
"home.recovery_hint": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate.",
"home.report_issue": "Raportează o problemă",
"home.samples": "Exemple",
"home.settings": "Setări",
"home.short_date": "{day} {month}",
"home.shortcuts": "Scurtături de la tastatură",
"home.start_hint": "Creează un proiect, deschide un fișier recent sau explorează un exemplu.",
"home.whats_new": "Noutăți",
"home.yesterday": "Ieri",
"install.button_tip": "Instalați SolidCraft ca aplicație: se deschide în propria fereastră și funcționează offline",
"install.dock_1": "Alegeți Fișier › Adaugă în Dock.",
"install.dock_2": "Faceți clic pe Adaugă.",
@@ -1021,7 +1059,7 @@
"search.try": "Tastați numele unei comenzi, o operație din acest proiect sau un parametru. Pentru a executa o comandă cu valori, tastați id-ul ei și un obiect JSON: {example}",
"search.valid": "Valid",
"settings.display_quality_note": "Nivelurile mai fine desenează fețele și muchiile curbe mai neted și durează mai mult la generarea plasei. Se schimbă doar fereastra de vizualizare, niciodată proiectul sau exporturile.",
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării. Cât timp este activat, vizualizarea se redesenează continuu.",
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării.",
"settings.measure_axis_distances": "Afișează distanțele X/Y/Z",
"settings.measure_axis_distances_help": "Afișează cotele pe axele globale alături de distanța totală cât timp Măsurare este deschis.",
"settings.msaa_applies_on_restart": "Anti-aliasing se aplică la următoarea pornire a SolidCraft",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Cel mult {max} fișiere merg cu un mesaj; omise: {files}",
"ui.agent_chat_files_loading": "Se așteaptă citirea fișierelor",
"ui.agent_chat_files_max": "Cel mult {max} fișiere pe un mesaj",
"ui.agent_chat_flatpak_none_found": "Nu s-a găsit niciun agent de programare. Implicit, sandboxul Flatpak nu vede agenții instalați în dosarul personal și nu are acces la rețea. Pentru a permite Claude Code și adaptorul său ACP instalate prin nvm sau în ~/.local/bin, rulați această comandă într-un terminal, apoi reporniți SolidCraft. Ea acordă acces la programele și fișierele de autentificare enumerate, precum și acces la rețea:",
"ui.agent_chat_import_failed": "{name}: importul a eșuat: {error}",
"ui.agent_chat_intro": "Conversați cu un agent de programare pe care îl folosiți deja sau cu un endpoint de model ales de dumneavoastră. Lucrează la proiectul deschis prin instrumentele SolidCraft: modificările lui apar pe măsură ce le face și se anulează ca un singur pas per mesaj. Ce poate face fără să întrebe se stabilește în Preferințe > AI.",
"ui.agent_chat_key_in_env": "cheie setată",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Vederile laterale afișează grila pe planul vertical orientat spre ele, în loc de sol",
"ui.ground_shadow": "Umbra la sol",
"ui.groupSelected": "Grupează selecția",
"ui.hatch": "Hașură",
"ui.heading_add_ins": "EXTENSII",
"ui.heading_assemble": "ASAMBLARE",
"ui.heading_box": "CUTIE",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "Лицензия MIT",
"help.notice": "NOTICE",
"help.third_party": "Лицензии сторонних компонентов",
"home.back": "Назад к {name}",
"home.crashes": "Отчёты о сбоях",
"home.design_workspace": "Проектирование",
"home.documentation": "Документация",
"home.export": "Экспорт",
"home.export_hint": "Выберите формат для текущего проекта.",
"home.export_json_hint": "Редактируемый проект и история команд",
"home.export_mesh_hint": "Сетка для печати или обмена",
"home.export_step_hint": "Точная твердотельная геометрия",
"home.import": "Импорт…",
"home.import_hint": "STEP, IGES или сетка в новый проект",
"home.in_this_browser": "в этом браузере",
"home.missing": "отсутствует",
"home.month_01": "янв.",
"home.month_02": "февр.",
"home.month_03": "мар.",
"home.month_04": "апр.",
"home.month_05": "мая",
"home.month_06": "июн.",
"home.month_07": "июл.",
"home.month_08": "авг.",
"home.month_09": "сент.",
"home.month_10": "окт.",
"home.month_11": "нояб.",
"home.month_12": "дек.",
"home.new_inches": "Новый проект в дюймах",
"home.new_inches_hint": "Дюймы · откроется в рабочей области «Проектирование»",
"home.new_mm": "Новый проект",
"home.new_mm_hint": "Миллиметры · откроется в рабочей области «Проектирование»",
"home.open": "Открыть…",
"home.opened_days_ago": "Открыт {count} дн. назад",
"home.opened_hours_ago": "Открыт {count} ч назад",
"home.opened_just_now": "Открыт только что",
"home.opened_minutes_ago": "Открыт {count} мин назад",
"home.open_hint": "Проект .solidcraft ({shortcut})",
"home.open_now": "Открыть сейчас",
"home.opened_days_ago": "{count} дн. назад",
"home.opened_hours_ago": "{count} ч назад",
"home.opened_just_now": "Только что",
"home.opened_minutes_ago": "{count} мин назад",
"home.opening": "открывается…",
"home.properties": "Свойства проекта",
"home.recent": "Недавние",
"home.recent_empty": "Здесь появятся проекты, которые вы откроете или сохраните.",
"home.recovery_count": "Проектов для восстановления: {count}",
"home.recovery_empty": "Нет проектов для восстановления.",
"home.recovery_hint": "SolidCraft закрылся до сохранения этих проектов.",
"home.report_issue": "Сообщить о проблеме",
"home.samples": "Примеры",
"home.settings": "Настройки",
"home.short_date": "{day} {month}",
"home.shortcuts": "Сочетания клавиш",
"home.start_hint": "Создайте проект, откройте недавний файл или изучите пример.",
"home.whats_new": "Что нового",
"home.yesterday": "Вчера",
"install.button_tip": "Установить SolidCraft как приложение: отдельное окно и работа без сети",
"install.dock_1": "Выберите «Файл» › «Добавить в Dock».",
"install.dock_2": "Нажмите «Добавить».",
@@ -1021,7 +1059,7 @@
"search.try": "Введите название команды, операцию этого дизайна или параметр. Чтобы выполнить команду со значениями, введите её id и объект JSON: {example}",
"search.valid": "Допустимо",
"settings.display_quality_note": "Более высокие уровни рисуют криволинейные грани и рёбра глаже, но сетка строится дольше. Меняется только видовой экран, но не дизайн и не экспорт.",
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида. Пока включено, вид перерисовывается непрерывно.",
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида.",
"settings.measure_axis_distances": "Расстояния X/Y/Z",
"settings.measure_axis_distances_help": "Показывать размеры вдоль мировых осей вместе с общим расстоянием, пока открыто окно измерения.",
"settings.msaa_applies_on_restart": "Сглаживание вступит в силу при следующем запуске SolidCraft",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "К одному сообщению можно приложить не более {max} файлов; не добавлены: {files}",
"ui.agent_chat_files_loading": "Ожидание чтения файлов",
"ui.agent_chat_files_max": "Не более {max} файлов в одном сообщении",
"ui.agent_chat_flatpak_none_found": "Кодирующий агент не найден. По умолчанию песочница Flatpak не видит агентов, установленных в домашней папке, и не имеет доступа к сети. Чтобы разрешить Claude Code и его адаптер ACP, установленные через nvm или в ~/.local/bin, выполните эту команду в терминале и перезапустите SolidCraft. Она даёт доступ к перечисленным программам и файлам входа, а также доступ к сети:",
"ui.agent_chat_import_failed": "{name}: не удалось импортировать: {error}",
"ui.agent_chat_intro": "Общайтесь с уже используемым кодирующим агентом или выбранной конечной точкой модели. Он работает с открытым дизайном через инструменты SolidCraft: изменения видны по мере выполнения и отменяются одним шагом на сообщение. Что ему разрешено без запроса, задаётся в Настройки > ИИ.",
"ui.agent_chat_key_in_env": "ключ задан",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "На боковых видах сетка показывается на вертикальной плоскости, обращённой к вам, а не на земле",
"ui.ground_shadow": "Тень на земле",
"ui.groupSelected": "Группировать выбранное",
"ui.hatch": "Штриховка",
"ui.heading_add_ins": "НАДСТРОЙКИ",
"ui.heading_assemble": "СБОРКА",
"ui.heading_box": "БОКС",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "MIT-licens",
"help.notice": "NOTICE",
"help.third_party": "Licenser från tredje part",
"home.back": "Tillbaka till {name}",
"home.crashes": "Kraschrapporter",
"home.design_workspace": "Konstruktion",
"home.documentation": "Dokumentation",
"home.export": "Exportera",
"home.export_hint": "Välj ett format för den aktuella konstruktionen.",
"home.export_json_hint": "Redigerbar konstruktion och kommandohistorik",
"home.export_mesh_hint": "Mesh för utskrift eller delning",
"home.export_step_hint": "Exakt solidgeometri",
"home.import": "Importera…",
"home.import_hint": "STEP, IGES eller mesh till en ny konstruktion",
"home.in_this_browser": "i den här webbläsaren",
"home.missing": "saknas",
"home.month_01": "jan.",
"home.month_02": "feb.",
"home.month_03": "mars",
"home.month_04": "apr.",
"home.month_05": "maj",
"home.month_06": "juni",
"home.month_07": "juli",
"home.month_08": "aug.",
"home.month_09": "sep.",
"home.month_10": "okt.",
"home.month_11": "nov.",
"home.month_12": "dec.",
"home.new_inches": "Ny konstruktion i tum",
"home.new_inches_hint": "Tum · öppnas i Konstruktion",
"home.new_mm": "Ny konstruktion",
"home.new_mm_hint": "Millimeter · öppnas i Konstruktion",
"home.open": "Öppna…",
"home.opened_days_ago": "Öppnades för {count} dagar sedan",
"home.opened_hours_ago": "Öppnades för {count} h sedan",
"home.opened_just_now": "Öppnades nyss",
"home.opened_minutes_ago": "Öppnades för {count} min sedan",
"home.open_hint": "En .solidcraft-konstruktion ({shortcut})",
"home.open_now": "Öppen nu",
"home.opened_days_ago": "För {count} dagar sedan",
"home.opened_hours_ago": "För {count} h sedan",
"home.opened_just_now": "Nyss",
"home.opened_minutes_ago": "För {count} min sedan",
"home.opening": "öppnar…",
"home.properties": "Konstruktionsegenskaper",
"home.recent": "Senaste",
"home.recent_empty": "Konstruktioner som du öppnar eller sparar visas här.",
"home.recovery_count": "Återställningsbara konstruktioner: {count}",
"home.recovery_empty": "Inga återställningsbara konstruktioner.",
"home.recovery_hint": "SolidCraft stängdes innan dessa konstruktioner sparades.",
"home.report_issue": "Rapportera ett problem",
"home.samples": "Exempel",
"home.settings": "Inställningar",
"home.short_date": "{day} {month}",
"home.shortcuts": "Kortkommandon",
"home.start_hint": "Skapa en konstruktion, öppna en nyligen använd fil eller utforska ett exempel.",
"home.whats_new": "Nyheter",
"home.yesterday": "I går",
"install.button_tip": "Installera SolidCraft som en app: den öppnas i ett eget fönster och fungerar offline",
"install.dock_1": "Välj Arkiv › Lägg till i Dock.",
"install.dock_2": "Klicka på Lägg till.",
@@ -1021,7 +1059,7 @@
"search.try": "Skriv ett kommandonamn, en funktion i denna konstruktion eller en parameter. För att köra ett kommando med värden, skriv dess id och ett JSON-objekt: {example}",
"search.valid": "Giltig",
"settings.display_quality_note": "Finare nivåer ritar krökta ytor och kanter jämnare och tar längre tid att mesha. Endast vyporten ändras, aldrig konstruktionen eller exporter.",
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn. Medan det är på ritas vyn om kontinuerligt.",
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn.",
"settings.measure_axis_distances": "Visa X/Y/Z-avstånd",
"settings.measure_axis_distances_help": "Visa världsaxelns komponentmått bredvid det totala avståndet medan Mät är öppet.",
"settings.msaa_applies_on_restart": "Kantutjämning tillämpas nästa gång SolidCraft startar",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Högst {max} filer följer med ett meddelande; utelämnade: {files}",
"ui.agent_chat_files_loading": "Väntar på att filerna ska läsas",
"ui.agent_chat_files_max": "Högst {max} filer i ett meddelande",
"ui.agent_chat_flatpak_none_found": "Ingen kodningsagent hittades. Flatpak-sandlådan ser som standard inte agenter som är installerade i din hemmapp och har ingen nätverksåtkomst. För att tillåta Claude Code och dess ACP-adapter installerade via nvm eller i ~/.local/bin kör du det här kommandot i en terminal och startar sedan om SolidCraft. Det ger åtkomst till de angivna programmen och inloggningsfilerna samt nätverksåtkomst:",
"ui.agent_chat_import_failed": "{name}: importen misslyckades: {error}",
"ui.agent_chat_intro": "Chatta med en kodningsagent du redan använder, eller en modellslutpunkt du väljer. Den arbetar med den öppna konstruktionen via SolidCrafts verktyg: dess ändringar syns medan den gör dem och ångras som ett steg per meddelande. Vad den får göra utan att fråga ställs in under Inställningar > AI.",
"ui.agent_chat_key_in_env": "nyckel angiven",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Sidovyer visar rutnätet på det upprätta planet som vetter mot dem istället för marken",
"ui.ground_shadow": "Markskugga",
"ui.groupSelected": "Gruppera markerade",
"ui.hatch": "Skraffering",
"ui.heading_add_ins": "TILLÄGG",
"ui.heading_assemble": "MONTERA",
"ui.heading_box": "LÅDA",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "MIT lisansı",
"help.notice": "NOTICE",
"help.third_party": "Üçüncü taraf lisansları",
"home.back": "{name} tasarımına dön",
"home.crashes": "Çökme raporları",
"home.design_workspace": "Tasarım",
"home.documentation": "Belgeler",
"home.export": "Dışa aktar",
"home.export_hint": "Geçerli tasarım için bir biçim seçin.",
"home.export_json_hint": "Düzenlenebilir tasarım ve komut geçmişi",
"home.export_mesh_hint": "Yazdırma veya paylaşma için ağ",
"home.export_step_hint": "Hassas katı geometrisi",
"home.import": "İçe aktar…",
"home.import_hint": "STEP, IGES veya ağı yeni bir tasarıma aktar",
"home.in_this_browser": "bu tarayıcıda",
"home.missing": "eksik",
"home.month_01": "Oca",
"home.month_02": "Şub",
"home.month_03": "Mar",
"home.month_04": "Nis",
"home.month_05": "May",
"home.month_06": "Haz",
"home.month_07": "Tem",
"home.month_08": "Ağu",
"home.month_09": "Eyl",
"home.month_10": "Eki",
"home.month_11": "Kas",
"home.month_12": "Ara",
"home.new_inches": "Yeni tasarım, inç",
"home.new_inches_hint": "İnç · Tasarım alanında açılır",
"home.new_mm": "Yeni tasarım",
"home.new_mm_hint": "Milimetre · Tasarım alanında açılır",
"home.open": "Aç…",
"home.opened_days_ago": "{count} gün önce açıldı",
"home.opened_hours_ago": "{count} sa. önce açıldı",
"home.opened_just_now": "Az önce açıldı",
"home.opened_minutes_ago": "{count} dk. önce açıldı",
"home.open_hint": "Bir .solidcraft tasarımı ({shortcut})",
"home.open_now": "Şimdi aç",
"home.opened_days_ago": "{count} gün önce",
"home.opened_hours_ago": "{count} sa. önce",
"home.opened_just_now": "Az önce",
"home.opened_minutes_ago": "{count} dk. önce",
"home.opening": "açılıyor…",
"home.properties": "Tasarım özellikleri",
"home.recent": "Son kullanılanlar",
"home.recent_empty": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
"home.recovery_count": "Kurtarılabilir tasarımlar: {count}",
"home.recovery_empty": "Kurtarılabilir tasarım yok.",
"home.recovery_hint": "Bu tasarımlar kaydedilmeden SolidCraft kapandı.",
"home.report_issue": "Sorun bildir",
"home.samples": "Örnekler",
"home.settings": "Ayarlar",
"home.short_date": "{day} {month}",
"home.shortcuts": "Klavye kısayolları",
"home.start_hint": "Bir tasarım oluşturun, son kullanılan bir dosyayı açın veya bir örneği inceleyin.",
"home.whats_new": "Yenilikler",
"home.yesterday": "Dün",
"install.button_tip": "SolidCraft'ı uygulama olarak yükleyin: kendi penceresinde açılır ve çevrimdışı da çalışır",
"install.dock_1": "Dosya › Dock'a Ekle'yi seçin.",
"install.dock_2": "“Ekle” düğmesine tıklayın.",
@@ -1021,7 +1059,7 @@
"search.try": "Bir komut adı, bu tasarımdaki bir özellik veya bir parametre yazın. Bir komutu değerlerle çalıştırmak için kimliğini ve bir JSON nesnesi yazın: {example}",
"search.valid": "Geçerli",
"settings.display_quality_note": "Daha ince seviyeler eğri yüzeyleri ve kenarları daha pürüzsüz gösterir, ancak ağ oluşturma daha uzun sürer. Yalnızca görünüm değişir; tasarım ve dışa aktarımlar asla değişmez.",
"settings.frame_rate_note": "Görünümün sağ alt köşesinde saniyedeki kare sayısını gösterir. Açıkken görünüm sürekli yeniden çizilir.",
"settings.frame_rate_note": "Görünümün sağ alt köşesinde saniyedeki kare sayısını gösterir.",
"settings.measure_axis_distances": "X/Y/Z mesafelerini göster",
"settings.measure_axis_distances_help": "Ölç açıkken toplam mesafenin yanında dünya ekseni bileşen ölçülerini gösterir.",
"settings.msaa_applies_on_restart": "Kenar yumuşatma, SolidCraft bir sonraki başlatıldığında uygulanır",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "Bir iletiyle en fazla {max} dosya gider; dışarıda kalanlar: {files}",
"ui.agent_chat_files_loading": "Dosyaların okunması bekleniyor",
"ui.agent_chat_files_max": "Bir iletide en fazla {max} dosya olabilir",
"ui.agent_chat_flatpak_none_found": "Kodlama ajanı bulunamadı. Flatpak korumalı alanı varsayılan olarak ana klasörünüze yüklenen ajanları göremez ve ağ erişimi yoktur. nvm ile veya ~/.local/bin içine yüklenen Claude Code'a ve ACP bağdaştırıcısına izin vermek için bu komutu bir terminalde çalıştırın, ardından SolidCraft'ı yeniden başlatın. Komut, listelenen programlara ve oturum açma dosyalarına erişimin yanı sıra ağ erişimi de verir:",
"ui.agent_chat_import_failed": "{name}: içe aktarma başarısız oldu: {error}",
"ui.agent_chat_intro": "Zaten kullandığınız bir kodlama ajanıyla veya seçtiğiniz bir model uç noktasıyla sohbet edin. Ajan, SolidCraft'ın araçlarıyla açık tasarım üzerinde çalışır: Değişiklikleri hemen görünür ve mesaj başına tek adımda geri alınabilir. Sormadan neleri yapabileceğini Ayarlar > AI altında belirlersiniz.",
"ui.agent_chat_key_in_env": "anahtar ayarlı",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "Yan görünümlerde ızgara, zemin yerine onlara bakan dik düzlemde gösterilir",
"ui.ground_shadow": "Zemin gölgesi",
"ui.groupSelected": "Seçimi grupla",
"ui.hatch": "Tarama",
"ui.heading_add_ins": "EKLENTİLER",
"ui.heading_assemble": "MONTAJ",
"ui.heading_box": "KUTU",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Ліцензія MIT",
"help.notice": "NOTICE",
"help.third_party": "Ліцензії сторонніх компонентів",
"home.back": "Назад до {name}",
"home.crashes": "Звіти про збої",
"home.design_workspace": "Проєктування",
"home.documentation": "Документація",
"home.export": "Експортувати",
"home.export_hint": "Виберіть формат для поточного проєкту.",
"home.export_json_hint": "Редагований проєкт та історія команд",
"home.export_mesh_hint": "Сітка для друку або поширення",
"home.export_step_hint": "Точна твердотільна геометрія",
"home.import": "Імпортувати…",
"home.import_hint": "STEP, IGES або сітку в новий проєкт",
"home.in_this_browser": "у цьому браузері",
"home.missing": "відсутній",
"home.month_01": "січ.",
"home.month_02": "лют.",
"home.month_03": "бер.",
"home.month_04": "квіт.",
"home.month_05": "трав.",
"home.month_06": "черв.",
"home.month_07": "лип.",
"home.month_08": "серп.",
"home.month_09": "вер.",
"home.month_10": "жовт.",
"home.month_11": "лист.",
"home.month_12": "груд.",
"home.new_inches": "Новий проєкт у дюймах",
"home.new_inches_hint": "Дюйми · відкриється в робочому просторі «Проєктування»",
"home.new_mm": "Новий проєкт",
"home.new_mm_hint": "Міліметри · відкриється в робочому просторі «Проєктування»",
"home.open": "Відкрити…",
"home.opened_days_ago": "Відкрито {count} дн. тому",
"home.opened_hours_ago": "Відкрито {count} год тому",
"home.opened_just_now": "Щойно відкрито",
"home.opened_minutes_ago": "Відкрито {count} хв тому",
"home.open_hint": "Проєкт .solidcraft ({shortcut})",
"home.open_now": "Відкрити зараз",
"home.opened_days_ago": "{count} дн. тому",
"home.opened_hours_ago": "{count} год тому",
"home.opened_just_now": "Щойно",
"home.opened_minutes_ago": "{count} хв тому",
"home.opening": "відкриття…",
"home.properties": "Властивості проєкту",
"home.recent": "Недавні",
"home.recent_empty": "Тут з’являться проєкти, які ви відкриєте або збережете.",
"home.recovery_count": "Проєктів для відновлення: {count}",
"home.recovery_empty": "Немає проєктів для відновлення.",
"home.recovery_hint": "SolidCraft закрився до збереження цих проєктів.",
"home.report_issue": "Повідомити про проблему",
"home.samples": "Зразки",
"home.settings": "Налаштування",
"home.short_date": "{day} {month}",
"home.shortcuts": "Сполучення клавіш",
"home.start_hint": "Створіть проєкт, відкрийте нещодавній файл або перегляньте приклад.",
"home.whats_new": "Що нового",
"home.yesterday": "Учора",
"install.button_tip": "Встановити SolidCraft як застосунок: він відкривається в окремому вікні й працює без мережі",
"install.dock_1": "Виберіть «Файл» › «Додати в Dock».",
"install.dock_2": "Натисніть «Додати».",
@@ -1022,7 +1060,7 @@
"search.try": "Введіть назву команди, елемент цього проєкту або параметр. Щоб виконати команду зі значеннями, введіть її id та об'єкт JSON: {example}",
"search.valid": "Дійсне",
"settings.display_quality_note": "Дрібніші рівні малюють криві грані й ребра плавнішими, але тріангуляція триває довше. Змінюється лише вікно перегляду, ніколи не проєкт чи експорт.",
"settings.frame_rate_note": "Показує кадри за секунду в правому нижньому куті виду. Поки це ввімкнено, вид перемальовується безперервно.",
"settings.frame_rate_note": "Показує кадри за секунду в правому нижньому куті виду.",
"settings.measure_axis_distances": "Відстані X/Y/Z",
"settings.measure_axis_distances_help": "Показувати розміри вздовж світових осей разом із загальною відстанню, поки відкрите вікно вимірювання.",
"settings.msaa_applies_on_restart": "Згладжування застосується під час наступного запуску SolidCraft",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "З одним повідомленням надсилається щонайбільше {max} файлів; пропущено: {files}",
"ui.agent_chat_files_loading": "Очікування читання файлів",
"ui.agent_chat_files_max": "Щонайбільше {max} файлів на одне повідомлення",
"ui.agent_chat_flatpak_none_found": "Агента для програмування не знайдено. Пісочниця Flatpak типово не бачить агентів, установлених у домашній теці, і не має доступу до мережі. Щоб дозволити Claude Code і його адаптер ACP, установлені через nvm або в ~/.local/bin, виконайте цю команду в терміналі, а потім перезапустіть SolidCraft. Вона надає доступ до перелічених програм і файлів входу, а також доступ до мережі:",
"ui.agent_chat_import_failed": "{name}: не вдалося імпортувати: {error}",
"ui.agent_chat_intro": "Спілкуйтеся з агентом для програмування, яким уже користуєтеся, або з обраною вами модельною кінцевою точкою. Він працює з відкритим проєктом через інструменти SolidCraft: його зміни з'являються в міру виконання й скасовуються одним кроком на повідомлення. Що він може робити без запиту, задано в Налаштування > ШІ.",
"ui.agent_chat_key_in_env": "ключ задано",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Бічні види показують сітку на вертикальній площині, повернутій до них, а не на землі",
"ui.ground_shadow": "Тінь на землі",
"ui.groupSelected": "Згрупувати виділене",
"ui.hatch": "Штрихування",
"ui.heading_add_ins": "НАДБУДОВИ",
"ui.heading_assemble": "СКЛАДАННЯ",
"ui.heading_box": "КУБ",
+45 -5
View File
@@ -857,19 +857,57 @@
"help.mit": "Giấy phép MIT",
"help.notice": "NOTICE",
"help.third_party": "Giấy phép của bên thứ ba",
"home.back": "Quay lại {name}",
"home.crashes": "Báo cáo sự cố",
"home.design_workspace": "Thiết kế",
"home.documentation": "Tài liệu hướng dẫn",
"home.export": "Xuất",
"home.export_hint": "Chọn định dạng cho thiết kế hiện tại.",
"home.export_json_hint": "Thiết kế có thể chỉnh sửa và lịch sử lệnh",
"home.export_mesh_hint": "Lưới để in hoặc chia sẻ",
"home.export_step_hint": "Hình học khối chính xác",
"home.import": "Nhập…",
"home.import_hint": "Nhập STEP, IGES hoặc lưới vào thiết kế mới",
"home.in_this_browser": "trong trình duyệt này",
"home.missing": "thiếu",
"home.month_01": "thg 1",
"home.month_02": "thg 2",
"home.month_03": "thg 3",
"home.month_04": "thg 4",
"home.month_05": "thg 5",
"home.month_06": "thg 6",
"home.month_07": "thg 7",
"home.month_08": "thg 8",
"home.month_09": "thg 9",
"home.month_10": "thg 10",
"home.month_11": "thg 11",
"home.month_12": "thg 12",
"home.new_inches": "Thiết kế mới theo inch",
"home.new_inches_hint": "Inch · mở trong Thiết kế",
"home.new_mm": "Thiết kế mới",
"home.new_mm_hint": "Milimét · mở trong Thiết kế",
"home.open": "Mở…",
"home.opened_days_ago": "Đã mở {count} ngày trước",
"home.opened_hours_ago": "Đã mở {count} giờ trước",
"home.opened_just_now": "Vừa mới mở",
"home.opened_minutes_ago": "Đã mở {count} phút trước",
"home.open_hint": "Một thiết kế .solidcraft ({shortcut})",
"home.open_now": "Mở ngay",
"home.opened_days_ago": "{count} ngày trước",
"home.opened_hours_ago": "{count} giờ trước",
"home.opened_just_now": "Vừa xong",
"home.opened_minutes_ago": "{count} phút trước",
"home.opening": "đang mở…",
"home.properties": "Thuộc tính thiết kế",
"home.recent": "Gần đây",
"home.recent_empty": "Các thiết kế bạn mở hoặc lưu sẽ xuất hiện ở đây.",
"home.recovery_count": "Thiết kế có thể khôi phục: {count}",
"home.recovery_empty": "Không có thiết kế nào có thể khôi phục.",
"home.recovery_hint": "SolidCraft đã đóng trước khi các thiết kế này được lưu.",
"home.report_issue": "Báo cáo sự cố",
"home.samples": "Mẫu thiết kế",
"home.settings": "Cài đặt",
"home.short_date": "{day} {month}",
"home.shortcuts": "Phím tắt",
"home.start_hint": "Tạo thiết kế, mở tệp gần đây hoặc khám phá mẫu.",
"home.whats_new": "Có gì mới",
"home.yesterday": "Hôm qua",
"install.button_tip": "Cài đặt SolidCraft dưới dạng ứng dụng: mở trong cửa sổ riêng và hoạt động ngoại tuyến",
"install.dock_1": "Chọn Tệp › Thêm vào Dock.",
"install.dock_2": "Nhấp vào Thêm.",
@@ -1022,7 +1060,7 @@
"search.try": "Nhập tên lệnh, một tính năng trong thiết kế này hoặc một tham số. Để chạy lệnh kèm giá trị, hãy nhập mã lệnh và một đối tượng JSON: {example}",
"search.valid": "Hợp lệ",
"settings.display_quality_note": "Mức độ chi tiết cao hơn giúp hiển thị các mặt cong và cạnh mượt mà hơn nhưng tốn nhiều thời gian tạo lưới hơn. Thiết lập này chỉ thay đổi vùng nhìn, không ảnh hưởng đến thiết kế hay dữ liệu xuất tệp.",
"settings.frame_rate_note": "Hiển thị số khung hình trên giây (FPS) ở góc dưới bên phải khung nhìn. Khi bật, khung nhìn sẽ vẽ lại liên tục.",
"settings.frame_rate_note": "Hiển thị số khung hình trên giây (FPS) ở góc dưới bên phải khung nhìn.",
"settings.measure_axis_distances": "Hiển thị khoảng cách theo trục X/Y/Z",
"settings.measure_axis_distances_help": "Hiển thị kích thước thành phần theo các trục tọa độ cùng với khoảng cách tổng thể khi mở công cụ Đo lường.",
"settings.msaa_applies_on_restart": "Khử răng cưa sẽ áp dụng vào lần khởi động tiếp theo của SolidCraft",
@@ -1145,6 +1183,7 @@
"ui.agent_chat_files_left_out": "Tối đa {max} tệp đính kèm trong một tin nhắn; đã bỏ qua: {files}",
"ui.agent_chat_files_loading": "Đang chờ đọc các tệp đính kèm",
"ui.agent_chat_files_max": "Tối đa {max} tệp trong một tin nhắn",
"ui.agent_chat_flatpak_none_found": "Không tìm thấy tác tử lập trình nào. Theo mặc định, hộp cát Flatpak không thấy các tác tử được cài trong thư mục nhà của bạn và không có quyền truy cập mạng. Để cho phép Claude Code và bộ điều hợp ACP của nó được cài qua nvm hoặc trong ~/.local/bin, hãy chạy lệnh này trong terminal, rồi khởi động lại SolidCraft. Lệnh cấp quyền truy cập các chương trình và tệp đăng nhập được liệt kê, cùng quyền truy cập mạng:",
"ui.agent_chat_import_failed": "{name}: nhập tệp thất bại: {error}",
"ui.agent_chat_intro": "Trò chuyện với tác tử lập trình (AI agent) bạn đang dùng hoặc một điểm cuối mô hình bạn chọn. AI thao tác trên thiết kế đang mở thông qua các công cụ của SolidCraft: các thay đổi hiển thị ngay khi thực hiện và có thể hoàn tác theo từng bước cho mỗi tin nhắn. Các thao tác AI được phép thực hiện mà không cần hỏi được đặt trong Tùy chọn cài đặt > AI.",
"ui.agent_chat_key_in_env": "đã đặt khóa",
@@ -1464,6 +1503,7 @@
"ui.grid_follows_view_hint": "Các góc nhìn cạnh hiển thị lưới trên mặt phẳng thẳng đứng hướng về phía camera thay vì trên mặt sàn",
"ui.ground_shadow": "Bóng đổ mặt sàn",
"ui.groupSelected": "Gộp nhóm vùng chọn",
"ui.hatch": "Gạch mặt cắt",
"ui.heading_add_ins": "TIỆN ÍCH MỞ RỘNG",
"ui.heading_assemble": "LẮP RÁP",
"ui.heading_box": "KHỐI HỘP",
+45 -5
View File
@@ -856,19 +856,57 @@
"help.mit": "MIT 授權",
"help.notice": "NOTICE",
"help.third_party": "第三方授權",
"home.back": "返回 {name}",
"home.crashes": "當機報告",
"home.design_workspace": "設計",
"home.documentation": "文件",
"home.export": "匯出",
"home.export_hint": "選擇目前設計的格式。",
"home.export_json_hint": "可編輯的設計與命令歷程",
"home.export_mesh_hint": "用於列印或分享的網格",
"home.export_step_hint": "精確的實體幾何",
"home.import": "匯入…",
"home.import_hint": "將 STEP、IGES 或網格匯入新設計",
"home.in_this_browser": "在此瀏覽器中",
"home.missing": "找不到檔案",
"home.month_01": "1月",
"home.month_02": "2月",
"home.month_03": "3月",
"home.month_04": "4月",
"home.month_05": "5月",
"home.month_06": "6月",
"home.month_07": "7月",
"home.month_08": "8月",
"home.month_09": "9月",
"home.month_10": "10月",
"home.month_11": "11月",
"home.month_12": "12月",
"home.new_inches": "新增設計(英吋)",
"home.new_inches_hint": "英吋 · 在設計工作區中開啟",
"home.new_mm": "新增設計",
"home.new_mm_hint": "公釐 · 在設計工作區中開啟",
"home.open": "開啟…",
"home.opened_days_ago": "{count} 天前開啟",
"home.opened_hours_ago": "{count} 小時前開啟",
"home.opened_just_now": "剛剛開啟",
"home.opened_minutes_ago": "{count} 分鐘前開啟",
"home.open_hint": ".solidcraft 設計檔案 ({shortcut})",
"home.open_now": "目前開啟中",
"home.opened_days_ago": "{count} 天前",
"home.opened_hours_ago": "{count} 小時前",
"home.opened_just_now": "剛剛",
"home.opened_minutes_ago": "{count} 分鐘前",
"home.opening": "正在開啟…",
"home.properties": "設計屬性",
"home.recent": "最近使用",
"home.recent_empty": "您開啟或儲存的設計會顯示於此。",
"home.recovery_count": "可復原的設計:{count} 個",
"home.recovery_empty": "沒有可復原的設計。",
"home.recovery_hint": "SolidCraft 在這些設計儲存前已關閉。",
"home.report_issue": "回報問題",
"home.samples": "範例",
"home.settings": "設定",
"home.short_date": "{month}{day}日",
"home.shortcuts": "鍵盤快速鍵",
"home.start_hint": "建立設計、開啟最近使用的檔案,或瀏覽範例。",
"home.whats_new": "新功能",
"home.yesterday": "昨天",
"install.button_tip": "將 SolidCraft 安裝為應用程式:在獨立視窗中開啟,離線也能使用",
"install.dock_1": "選擇「檔案」›「加入 Dock」。",
"install.dock_2": "按一下「加入」。",
@@ -1021,7 +1059,7 @@
"search.try": "輸入命令名稱、此設計中的特徵或參數。要帶值執行命令,請輸入其ID和JSON物件:{example}",
"search.valid": "有效",
"settings.display_quality_note": "等級越精細,曲面和邊顯示越平滑,但網格化耗時更長。只影響檢視區,不影響設計或匯出。",
"settings.frame_rate_note": "在檢視右下角顯示每秒影格數。開啟時檢視會持續重繪。",
"settings.frame_rate_note": "在檢視右下角顯示每秒影格數。",
"settings.measure_axis_distances": "顯示 X/Y/Z 距離",
"settings.measure_axis_distances_help": "開啟測量時,在總距離之外顯示沿世界座標軸方向的分量尺寸。",
"settings.msaa_applies_on_restart": "抗鋸齒將在下次啟動 SolidCraft 時生效",
@@ -1144,6 +1182,7 @@
"ui.agent_chat_files_left_out": "一條訊息最多傳送 {max} 個檔案;未傳送:{files}",
"ui.agent_chat_files_loading": "正在等待檔案讀取完成",
"ui.agent_chat_files_max": "一條訊息最多附加 {max} 個檔案",
"ui.agent_chat_flatpak_none_found": "未找到程式設計代理。預設情況下,Flatpak 沙箱無法存取主資料夾中安裝的代理,也無法存取網路。若要允許透過 nvm 或在 ~/.local/bin 中安裝的 Claude Code 及其 ACP 轉接器,請在終端機執行以下命令,然後重新啟動 SolidCraft。此命令會授予對所列程式和登入檔案的存取權限,以及網路存取權限:",
"ui.agent_chat_import_failed": "{name}:匯入失敗:{error}",
"ui.agent_chat_intro": "與您已在使用的程式設計代理或您選擇的模型端點聊天。它透過 SolidCraft 的工具處理開啟的設計:變更即時顯示,每條訊息的變更可一步復原。無需詢問即可執行的範圍在「偏好設定 > AI」中設定。",
"ui.agent_chat_key_in_env": "已設定金鑰",
@@ -1463,6 +1502,7 @@
"ui.grid_follows_view_hint": "在側檢視中,將網格顯示在朝向該檢視的豎直平面上,而不是地面上",
"ui.ground_shadow": "地面陰影",
"ui.groupSelected": "群組所選",
"ui.hatch": "剖面線",
"ui.heading_add_ins": "增益集",
"ui.heading_assemble": "組裝",
"ui.heading_box": "長方體",
+328 -6
View File
@@ -14,11 +14,15 @@
"Active": "活动",
"Active rule": "活动规则",
"Add": "添加",
"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": "所有草图点",
@@ -32,6 +36,7 @@
"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": "弧长",
@@ -42,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": "检查",
@@ -60,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",
@@ -69,15 +81,21 @@
"Chamfer Type": "倒角类型",
"Check collisions": "检查碰撞",
"Checked": "已检查",
"Choose Variable radius or Spline in Type.": "请在“类型”中选择 Variable radius 或样条曲线。",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
"Choose…": "选择…",
"Circular": "圆周",
"Circumference": "周长",
"Close draft": "关闭草稿",
"Collapse": "折叠",
"Collisions": "碰撞",
"Colour": "颜色",
"Command": "命令",
"Comment": "注释",
"Component 1": "组件 1",
"Component 2": "组件 2",
"Component1": "组件1",
"Components": "组件",
"Config Table": "配置表",
"Configuration": "配置",
"Configuration rules": "配置规则",
@@ -86,6 +104,7 @@
"Contact sets": "接触集",
"Continue on GitHub": "在GitHub中继续",
"Continuity": "连续性",
"Control Radius": "控制半径",
"Copy Error Message": "复制错误消息",
"Copy Warning Message": "复制警告消息",
"Copy all licences": "复制所有许可证",
@@ -95,8 +114,11 @@
"Corner Relief": "拐角释放",
"Corner Relief Shape": "拐角释放形状",
"Counterbore depth": "沉孔深度",
"Counterbore Ø": "柱形沉孔 Ø",
"Countersink angle": "沉头角度",
"Countersink Ø": "锥形沉孔 Ø",
"Create Issue — review draft": "创建 Issue——审阅草稿",
"Create Issue…": "创建 Issue…",
"Cross curves": "交叉曲线",
"Cube": "立方体",
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
@@ -106,7 +128,11 @@
"Curvature change (G3)": "曲率变化 (G3)",
"Custom": "自定义",
"Customize Toolbar": "自定义工具栏",
"Customize…": "自定义…",
"Cylindrical face": "圆柱面",
"Delete": "删除",
"Delete Selected Features…": "删除所选特征…",
"Delete Selected Folders": "删除所选文件夹",
"Delete all": "全部删除",
"Delete rule": "删除规则",
"Delete the row": "删除此行",
@@ -117,8 +143,11 @@
"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": "双击重命名",
@@ -130,44 +159,57 @@
"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": "以虚线导出隐藏线",
"Exported by SolidWorks, which usually saves Y up": "由 SolidWorks 导出,通常以 Y 轴向上保存",
"Expression": "表达式",
"Extend to meet": "延伸至相交",
"Extents": "范围",
"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 fit。尚不支持构建 G2 拐角。",
"Gap": "间隙",
"Grid": "网格",
"HISTORY": "历史",
@@ -175,6 +217,7 @@
"Hidden edges": "隐藏线",
"History layout": "历史布局",
"History names": "历史名称",
"Hole (optional)": "孔(可选)",
"Holes": "孔",
"Holes (edges on them)": "孔(孔上的边)",
"Home view": "主视图",
@@ -185,14 +228,20 @@
"In place (surfaces)": "原位(曲面)",
"In plane": "在平面内",
"Inside": "内侧",
"Instant": "即时",
"Interface font": "界面字体",
"Interferes": "发生干涉",
"Invert": "反转",
"Isocurve": "等参线",
"Issue title": "Issue 标题",
"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": "稍后",
@@ -203,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": "模型",
@@ -216,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": "下移",
@@ -246,8 +306,11 @@
"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.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
"Opens your browser and copies the report text for you to paste into GitHub.": "打开浏览器,并复制报告文本以便粘贴到GitHub。",
@@ -269,8 +332,10 @@
"Picked": "所选",
"Picked point": "选取的点",
"Pitch": "螺距",
"Pivot": "枢轴",
"Place at the origin instead": "改为放置在原点",
"Plastic Rules": "塑料规则",
"Play the joint's motion in the view": "在视图中播放接头运动",
"Playback": "播放",
"Plot": "图表",
"Position": "位置",
@@ -278,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": "重新附着所选面",
@@ -294,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.": "共享前请移除私密设计细节或路径。",
@@ -301,6 +372,9 @@
"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账户。",
@@ -309,6 +383,7 @@
"Restore View": "恢复视图",
"Restore outer boundary": "恢复外边界",
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
"Restore this studio's tools": "恢复此工作室的工具",
"Result": "结果",
"Retake screenshot": "重新截图",
"Reverse": "反向",
@@ -319,7 +394,13 @@
"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": "规则",
@@ -338,13 +419,18 @@
"Search": "搜索",
"Search commands": "搜索命令",
"Search this library": "搜索此库",
"Second plane": "第二个平面",
"Section": "剖面",
"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": "显示代理光标",
@@ -362,6 +448,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
"Specialise": "专用操作",
"Speed": "速度",
"Splitting tool": "分割工具",
"Stand it up": "将其立起",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
"Stand this part up?": "立起此零件?",
@@ -372,16 +459,22 @@
"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.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
@@ -396,24 +489,31 @@
"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": "视图导航器",
@@ -427,13 +527,44 @@
"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": "电介质折射率",
"appearance.metallic": "金属度",
@@ -479,8 +610,10 @@
"cmd.component_activate": "激活组件",
"cmd.component_break_link": "断开链接",
"cmd.component_capture_position": "捕获位置",
"cmd.component_copies": "查找相同副本",
"cmd.component_create": "新建组件",
"cmd.component_delete": "删除组件",
"cmd.component_extract": "提取到文件",
"cmd.component_from_bodies": "由实体创建组件",
"cmd.component_insert_design": "插入设计",
"cmd.component_links": "列出链接",
@@ -493,6 +626,7 @@
"cmd.component_properties": "组件物理属性",
"cmd.component_relink": "重新链接源文件",
"cmd.component_rename": "重命名组件",
"cmd.component_replace_with_link": "替换为链接",
"cmd.component_revert_position": "恢复位置",
"cmd.component_unlock": "解锁内容",
"cmd.component_update_links": "更新链接",
@@ -610,6 +744,7 @@
"cmd.motion_plot": "运动研究图表",
"cmd.motion_study": "运动研究",
"cmd.motion_trace": "运动轨迹",
"cmd.motion_trace_edit": "编辑运动轨迹",
"cmd.motion_trace_keep": "保留运动轨迹",
"cmd.occurrence_copy": "粘贴(实例)",
"cmd.occurrence_ground": "固定",
@@ -889,19 +1024,57 @@
"help.solid.revolve": "将草图轮廓绕一条轴旋转,生成圆形的实体。用于轴、旋钮、碗等围绕中心线呈圆形的零件。",
"help.solid.shell": "将实体掏空,留下指定厚度的壁,还可以打开所选的面。用于盒子、盖子等薄壁零件。",
"help.third_party": "第三方许可证",
"home.back": "返回 {name}",
"home.crashes": "崩溃报告",
"home.design_workspace": "设计",
"home.documentation": "文档",
"home.export": "导出",
"home.export_hint": "为当前设计选择一种格式。",
"home.export_json_hint": "可编辑的设计和命令历史记录",
"home.export_mesh_hint": "用于打印或共享的网格",
"home.export_step_hint": "精确的实体几何",
"home.import": "导入…",
"home.import_hint": "将 STEP、IGES 或网格导入新设计",
"home.in_this_browser": "在此浏览器中",
"home.missing": "文件不存在",
"home.month_01": "1月",
"home.month_02": "2月",
"home.month_03": "3月",
"home.month_04": "4月",
"home.month_05": "5月",
"home.month_06": "6月",
"home.month_07": "7月",
"home.month_08": "8月",
"home.month_09": "9月",
"home.month_10": "10月",
"home.month_11": "11月",
"home.month_12": "12月",
"home.new_inches": "新建设计(英寸)",
"home.new_inches_hint": "英寸 · 在设计工作区中打开",
"home.new_mm": "新建设计",
"home.new_mm_hint": "毫米 · 在设计工作区中打开",
"home.open": "打开…",
"home.opened_days_ago": "{count} 天前打开",
"home.opened_hours_ago": "{count} 小时前打开",
"home.opened_just_now": "刚刚打开",
"home.opened_minutes_ago": "{count} 分钟前打开",
"home.open_hint": ".solidcraft 设计文件 ({shortcut})",
"home.open_now": "立即打开",
"home.opened_days_ago": "{count} 天前",
"home.opened_hours_ago": "{count} 小时前",
"home.opened_just_now": "刚刚",
"home.opened_minutes_ago": "{count} 分钟前",
"home.opening": "正在打开…",
"home.properties": "设计属性",
"home.recent": "最近使用",
"home.recent_empty": "您打开或保存的设计将显示在这里。",
"home.recovery_count": "可恢复的设计: {count} 个",
"home.recovery_empty": "没有可恢复的设计。",
"home.recovery_hint": "SolidCraft 在这些设计保存前已关闭。",
"home.report_issue": "报告问题",
"home.samples": "示例",
"home.settings": "设置",
"home.short_date": "{month}{day}日",
"home.shortcuts": "键盘快捷键",
"home.start_hint": "创建设计、打开最近的文件或浏览示例。",
"home.whats_new": "新功能",
"home.yesterday": "昨天",
"install.button_tip": "将 SolidCraft 安装为应用:在独立窗口中打开,离线也能使用",
"install.dock_1": "选择“文件”›“添加到程序坞”。",
"install.dock_2": "点按“添加”。",
@@ -959,8 +1132,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 并移动,无需鼠标按钮)",
@@ -983,8 +1163,130 @@
"pattern.flip_direction": "反转方向",
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
"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": "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并用鼠标左键拖动可旋转;按住空格键可平移。",
"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 −。100% 时按钮高度为 28 像素。",
"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": "以 Z 轴向上打开 STEP 或 IGES 文件后,询问是否绕 X 轴旋转 90°。",
"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}",
@@ -1055,8 +1357,8 @@
"search.suggest": "您是否想找 {label}?",
"search.try": "输入命令名称、此设计中的特征或参数。要带值运行命令,请输入其ID和JSON对象:{example}",
"search.valid": "有效",
"settings.display_quality_note": "级别越精细,曲面和边显示越平滑,但网格生成耗时更长。只影响视口,不影响设计或导出。",
"settings.frame_rate_note": "在视图右下角显示每秒帧数。开启时视图会持续重绘。",
"settings.display_quality_note": "仅影响显示,不影响设计和导出。",
"settings.frame_rate_note": "在视图右下角显示每秒帧数。",
"settings.measure_axis_distances": "显示 X/Y/Z 距离",
"settings.measure_axis_distances_help": "测量对话框打开时,在总距离之外显示沿世界坐标轴方向的尺寸线和距离。",
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
@@ -1065,6 +1367,7 @@
"settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:",
"settings.reset_title": "设置已更改",
"settings.supersampling_note": "以更高分辨率绘制视图再缩小:在低于 4K 的屏幕上边、线和网格更平滑。会消耗 GPU 时间:1.5× 时约 2.25 倍像素,2× 时 4 倍。",
"sheet.no_body": "没有钣金实体:请使用法兰 > Base 创建,或使用转换为钣金将板材转换为钣金(选择其较大的平面)。",
"shortcut.json_error": "参数必须采用 JSON 格式:{error}",
"shortcut.no_command": "没有命令 `{id}`;JSON 前的文本必须是命令 ID",
"shortcut.pin": "固定到 S 框顶部",
@@ -1101,6 +1404,9 @@
"thread.designation": "螺纹标记",
"thread.iso_metric": "ISO 公制",
"thread.select_size": "选择尺寸",
"timeline.delete_after_marker_one": "删除历史标记之后的 1 个特征",
"timeline.delete_after_marker_other": "删除历史标记之后的 {count} 个特征",
"timeline.delete_many": "删除所选的 {count} 个特征",
"timeline.group": "新建特征文件夹",
"timeline.move_many": "移动时间线项目",
"timeline.move_to_group": "将特征移至文件夹",
@@ -1179,6 +1485,7 @@
"ui.agent_chat_files_left_out": "一条消息最多发送 {max} 个文件;未发送:{files}",
"ui.agent_chat_files_loading": "正在等待文件读取完成",
"ui.agent_chat_files_max": "一条消息最多附加 {max} 个文件",
"ui.agent_chat_flatpak_none_found": "未找到编程代理。默认情况下,Flatpak 沙箱无法访问主目录中安装的代理,也无法访问网络。要允许使用通过 nvm 或 ~/.local/bin 安装的 Claude Code 及其 ACP 适配器,请在终端运行以下命令,然后重新启动 SolidCraft。此命令授予对所列程序和登录文件的访问权限以及网络访问权限:",
"ui.agent_chat_import_failed": "{name}:导入失败:{error}",
"ui.agent_chat_intro": "与你已在使用的编程智能体或你选择的模型端点聊天。它通过 SolidCraft 的工具处理打开的设计:更改即时显示,每条消息的更改可一步撤销。无需询问即可执行的范围在“首选项 > AI”中设置。",
"ui.agent_chat_key_in_env": "已设置密钥",
@@ -1251,8 +1558,10 @@
"ui.ai_add": "添加",
"ui.ai_add_agent": "添加智能体…",
"ui.ai_add_agent_title": "添加智能体",
"ui.ai_add_anthropic": "Anthropic 兼容 API",
"ui.ai_add_api": "OpenAI 兼容 API",
"ui.ai_add_custom": "自定义命令",
"ui.ai_add_endpoint": "API 端点",
"ui.ai_add_installed": "已安装的智能体",
"ui.ai_add_installed_note": "为已安装的智能体添加一个自己的条目,例如使用其他参数。已安装的智能体会自动列出。",
"ui.ai_added": "已添加",
@@ -1281,6 +1590,7 @@
"ui.ai_key_needs_https": "密钥只会通过 HTTPS 发送,或发送到本机。",
"ui.ai_key_note": "保存在系统钥匙串中,绝不保存在 SolidCraft 的设置里。本地服务器可留空。",
"ui.ai_keychain_error": "系统钥匙串拒绝:{error}",
"ui.ai_kind": "类型",
"ui.ai_mode_all": "全部允许",
"ui.ai_mode_all_note": "不再询问。崩溃报告仍不可访问,文件夹限制仍然有效。",
"ui.ai_mode_all_warning": "智能体可以不经询问保存、覆盖、导出和替换你的设计。只在你信任的智能体和已保存的设计上使用。",
@@ -1302,6 +1612,7 @@
"ui.ai_panel": "面板",
"ui.ai_permissions": "权限",
"ui.ai_preset": "预设",
"ui.ai_protocol": "协议",
"ui.ai_remove": "移除",
"ui.ai_request_limit": "每条消息 24 次请求后已停止;再发送一条消息即可继续",
"ui.ai_reset": "重置",
@@ -1312,6 +1623,8 @@
"ui.ai_test": "测试",
"ui.ai_test_acp_ok": "已响应:{agent}",
"ui.ai_test_acp_tip": "短暂启动智能体并请求一个会话,然后结束它",
"ui.ai_test_answered": "已连接:{model} 已响应",
"ui.ai_test_anthropic_tip": "向端点发送一条简短消息",
"ui.ai_test_api_tip": "向端点查询其模型列表",
"ui.ai_test_no_model": "已连接:{count} 个模型,但没有 {model}",
"ui.ai_test_ok": "已连接:{count} 个模型",
@@ -1469,6 +1782,9 @@
"ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件",
"ui.extend": "延伸",
"ui.extent_type": "延伸类型",
"ui.extract_component": "提取到文件…",
"ui.extract_design_changed": "选择文件时设计已更改。请重新开始提取到文件。",
"ui.extract_done": "该组件现在是独立的设计,并已在此处链接。",
"ui.extrude": "拉伸",
"ui.factor": "系数",
"ui.feature_history": "特征历史",
@@ -1500,6 +1816,7 @@
"ui.grid_follows_view_hint": "在侧视图中,将网格显示在朝向该视图的竖直平面上,而不是地面上",
"ui.ground_shadow": "地面阴影",
"ui.groupSelected": "将所选编组",
"ui.hatch": "剖面线",
"ui.heading_add_ins": "加载项",
"ui.heading_assemble": "装配",
"ui.heading_box": "长方体",
@@ -1607,6 +1924,10 @@
"ui.length_decimals": "长度小数位",
"ui.light": "浅色",
"ui.line": "直线",
"ui.link_copies": "链接相同副本",
"ui.link_copies_keep": "保留",
"ui.link_copies_link": "链接",
"ui.link_copies_text": "这些组件生成的实体与 {name} 相同。要将它们改为其链接的放置吗?",
"ui.lock": "锁定",
"ui.loft": "放样",
"ui.look_at": "朝向",
@@ -1730,6 +2051,7 @@
"ui.render": "渲染",
"ui.renderer": "渲染器",
"ui.repeat": "重复",
"ui.replace_with_link_done": "这些副本现在是链接设计的放置。",
"ui.report_a_bug": "报告缺陷",
"ui.reset": "重置",
"ui.reset_all": "全部重置",
+6 -1
View File
@@ -1,9 +1,14 @@
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
/// The default material's name: what a body without a usable material of its own is made of.
/// Assigning it clears a body's assignment.
pub const DEFAULT_MATERIAL: &str = "Default";
/// Density of the default material (g/cm³).
pub const DEFAULT_DENSITY: f64 = 1.29;
/// Physical materials: name and density (g/cm³).
pub const MATERIALS: [(&str, f64); 14] = [
("Default", DEFAULT_DENSITY),
(DEFAULT_MATERIAL, DEFAULT_DENSITY),
("Steel", 7.85),
("Stainless Steel", 8.0),
("Aluminum", 2.70),
+164 -4
View File
@@ -16,6 +16,8 @@ struct U {
ana_c: array<vec4<f32>, 8>,
ana_f: vec4<f32>, // x = 1: stripe the negative draft and blocked faces
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
tint: vec4<f32>, // xyz added to a whole hovered body's colour before lighting
sel: vec4<f32>, // xyz a whole selected body's colour before lighting
};
fn reversed() -> bool {
@@ -64,7 +66,138 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
return o;
}
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
/// Shared triangles (in their part's own frame) drawn at one placement: the rows of its rigid
/// move (`r0`..`r2`, a point's coordinates are their dot products with (p, 1)) and its colour.
/// The vertex colour (location 2) is not read.
@vertex
fn vs_tri_inst(
@location(0) p: vec3<f32>,
@location(1) n: vec3<f32>,
@location(3) r0: vec4<f32>,
@location(4) r1: vec4<f32>,
@location(5) r2: vec4<f32>,
@location(6) c: vec4<f32>,
) -> TOut {
let q = vec4<f32>(p, 1.0);
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
var o: TOut;
o.pos = u.vp * vec4<f32>(wp, 1.0);
o.n = vec3<f32>(dot(r0.xyz, n), dot(r1.xyz, n), dot(r2.xyz, n));
o.c = c;
o.wp = wp;
return o;
}
/// A flat-shaded placement (`vs_tri_inst_flat`): its vertices are shared between triangles, so
/// the face's normal is the triangle's own plane, worked out per fragment. The fragment gets its
/// position from the eye (`rp`): small near what is looked at, so the plane comes out exact at
/// any zoom however far from the origin the part stands, and few values per vertex (a tiled GPU
/// stores every vertex's outputs: more of them cost more than the triangles' shared vertices
/// save).
struct FOut {
@builtin(position) pos: vec4<f32>,
@location(0) rp: vec3<f32>,
@location(1) c: vec4<f32>,
};
@vertex
fn vs_tri_inst_flat(
@location(0) p: vec3<f32>,
@location(3) r0: vec4<f32>,
@location(4) r1: vec4<f32>,
@location(5) r2: vec4<f32>,
@location(6) c: vec4<f32>,
) -> FOut {
let q = vec4<f32>(p, 1.0);
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
var o: FOut;
o.pos = u.vp * vec4<f32>(wp, 1.0);
o.rp = wp - u.eye.xyz;
o.c = c;
return o;
}
/// The fragment of a flat-shaded placement as the other triangles' fragments are: its normal
/// the outward normal of its triangle (toward the eye on a front face, away on a back face, as
/// the triangles are wound outward).
fn flat_fragment(i: FOut, front: bool) -> TOut {
var n = cross(dpdx(i.rp), dpdy(i.rp));
let toward = select(u.back.xyz, -i.rp, u.eye.w > 0.5);
n = n * inverseSqrt(max(dot(n, n), 1e-30));
if ((dot(n, toward) < 0.0) == front) { n = -n; }
var o: TOut;
o.pos = i.pos;
o.n = n;
o.c = i.c;
o.wp = i.rp + u.eye.xyz;
return o;
}
@fragment
fn fs_solid_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let t = flat_fragment(i, front);
let c = solid(t, front);
return c;
}
@fragment
fn fs_solid_cut_flat(i: FOut, @builtin(front_facing) front: bool) -> CutOut {
let t = flat_fragment(i, front);
var o: CutOut;
o.depth = pushed(cap_depth(t.wp, t.pos.z));
o.c = solid(t, front);
return o;
}
/// Weighted blended order-independent transparency (`view/oit.rs`): a see-through fragment's
/// colour, premultiplied and weighted by its alpha and nearness, into the accumulation target,
/// and its alpha into the revealage target (which keeps what is behind it by 1 - alpha).
struct Oit {
@location(0) accum: vec4<f32>,
@location(1) reveal: vec4<f32>,
};
fn oit(c: vec4<f32>, z: f32) -> Oit {
let a = clamp(c.a, 0.0, 1.0);
// Depth 0 near and 1 far, whichever way the buffer runs.
let d = select(z, 1.0 - z, reversed());
let w = clamp(a * max(1e-2, 3e3 * pow(1.0 - d, 3.0)), 1e-2, 3e3);
var o: Oit;
o.accum = vec4<f32>(c.rgb * a, a) * w;
o.reveal = vec4<f32>(a);
return o;
}
@fragment
fn fs_oit(i: TOut) -> Oit {
if (clipped(i.wp)) { discard; }
return oit(shade(i), i.pos.z);
}
@fragment
fn fs_oit_flat(i: FOut, @builtin(front_facing) front: bool) -> Oit {
let t = flat_fragment(i, front);
let o = oit(shade(t), t.pos.z);
if (clipped(t.wp)) { discard; }
return o;
}
/// A see-through flat-shaded placement (faded or glass), shaded like `fs_tri`.
@fragment
fn fs_tri_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let t = flat_fragment(i, front);
let c = shade(t);
if (clipped(t.wp)) { discard; }
return c;
}
@fragment
fn fs_ao_depth_flat(i: FOut, @builtin(front_facing) front: bool) -> AoDepth {
let t = flat_fragment(i, front);
return opaque_depth(t, front);
}
/// Opaque model surfaces: cut by the section plane/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
@fragment
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let c = solid(i, front);
@@ -122,7 +255,9 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
if (clipped(i.wp)) { discard; }
if (!front && length(u.clip.xyz) > 0.5) {
// The cut face in its body's colour ("from component" colouring), tinted toward the
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
// cap colour so it reads as a cut, hatched at 45 degrees on screen; with the hatch off
// (`u.cap.a` 0), plain in the body's own colour.
if (u.cap.a < 0.5) { return out_color(vec4<f32>(i.c.rgb, 1.0)); }
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
let h = fract((i.pos.x + i.pos.y) / 9.0);
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
@@ -150,6 +285,11 @@ struct AoDepth {
@fragment
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
return opaque_depth(i, front);
}
/// The AO prepass's depth of an opaque surface's fragment.
fn opaque_depth(i: TOut, front: bool) -> AoDepth {
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
var depth = i.pos.z;
if (!front && length(u.clip.xyz) > 0.5) { depth = cap_depth(i.wp, i.pos.z); }
@@ -286,6 +426,20 @@ fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
return shade(i);
}
/// A whole hovered body, lit over the model from its own plan triangles in a lifted colour.
@fragment
fn fs_hl_hover(i: TOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return lit_color(i, 1.0, vec4<f32>(i.c.rgb + u.tint.rgb, i.c.a));
}
/// A whole selected body, lit over the model from its own plan triangles in the selection colour.
@fragment
fn fs_hl_selected(i: TOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return lit_color(i, 1.0, vec4<f32>(u.sel.rgb, i.c.a));
}
/// Reference planes: shaded like `fs_tri`, but never cut by the section plane.
@fragment
fn fs_tri_all(i: TOut) -> @location(0) vec4<f32> {
@@ -297,9 +451,15 @@ fn shade(i: TOut) -> vec4<f32> {
}
fn lit(i: TOut, visibility: f32) -> vec4<f32> {
return lit_color(i, visibility, i.c);
}
/// [`lit`] in a colour of its own: a whole highlighted body is lit in its colour lifted by the
/// hover amount, or in the selection colour, from the model's own plan triangles.
fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
// A zero normal marks flat colour (selection fills, translucent planes).
if (length(i.n) < 0.5) {
return out_color(i.c);
return out_color(base);
}
let v = normalize(u.back.xyz);
var n = normalize(i.n);
@@ -310,7 +470,7 @@ fn lit(i: TOut, visibility: f32) -> vec4<f32> {
let h = normalize(key + v);
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
let k = 0.42 + 0.38 * diff + 0.25 * fill - (0.42 + 0.25 * fill) * (1.0 - visibility);
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
return out_color(vec4<f32>(min(base.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
}
// Lines, decals and ghosts are drawn at their own depth: opaque faces are pushed back by their
+49 -7
View File
@@ -8,6 +8,35 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
impl Tokens {
pub const fn light() -> Tokens {
Tokens {
file_dim: Color32::from_rgb(58, 70, 86),
file_recovery_well: Color32::from_rgb(247, 226, 184),
file_action_height: 32.0,
file_back_height: 40.0,
file_recovery_icon: 32.0,
file_grid_gap: 14.0,
file_background: Color32::from_rgb(247, 249, 251),
file_surface: Color32::WHITE,
file_border: Color32::from_rgb(224, 229, 236),
file_image_tint: Color32::WHITE,
file_rail: Color32::from_rgb(35, 110, 173),
file_rail_ink: Color32::WHITE,
file_rail_hover: Color32::from_rgb(43, 118, 180),
file_primary: Color32::from_rgb(227, 240, 251),
file_primary_ink: Color32::from_rgb(26, 80, 127),
file_primary_border: Color32::from_rgb(50, 139, 211),
file_recovery: Color32::from_rgb(255, 246, 229),
file_recovery_ink: Color32::from_rgb(122, 74, 5),
file_recovery_border: Color32::from_rgb(240, 210, 154),
file_nav_width: 248.0,
file_nav_height: 38.0,
file_tile_height: 84.0,
file_tile_icon: 44.0,
file_recent_height: 52.0,
file_sample_image_height: 84.0,
file_content_inset_x: 40.0,
file_content_inset_y: 32.0,
file_section_gap: 28.0,
font_start_title: 28.0,
space_2: 2.0,
space_3: 3.0,
space_4: 4.0,
@@ -201,7 +230,7 @@ impl Tokens {
],
analysis: AnalysisColors::DEFAULT,
icons: IconColors { highlight: Color32::WHITE, agent: Color32::from_rgb(168, 85, 247), marker_halo: Color32::from_black_alpha(150) },
logo: [Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197), Color32::WHITE],
logo: [Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197)],
}
}
pub const fn dark() -> Tokens {
@@ -236,6 +265,19 @@ impl Tokens {
t.sketch_mode_tint = 0.12;
t.sketch_footer_icon_size = 15.0;
t.font_section_tracking = 0.04;
t.file_dim = Color32::from_rgb(180, 187, 200);
t.file_recovery_well = Color32::from_rgb(92, 68, 32);
t.file_background = Color32::from_rgb(31, 35, 43);
t.file_surface = Color32::from_rgb(39, 43, 52);
t.file_border = Color32::from_rgb(63, 69, 82);
t.file_rail = Color32::from_rgb(30, 80, 128);
t.file_rail_hover = Color32::from_rgb(46, 99, 149);
t.file_primary = Color32::from_rgb(27, 48, 72);
t.file_primary_ink = Color32::from_rgb(156, 199, 245);
t.file_primary_border = Color32::from_rgb(58, 136, 238);
t.file_recovery = Color32::from_rgb(67, 51, 28);
t.file_recovery_ink = Color32::from_rgb(245, 204, 137);
t.file_recovery_border = Color32::from_rgb(115, 88, 43);
t.app_bar = Color32::from_rgb(28, 31, 38);
t.toolbar = Color32::from_rgb(39, 43, 52);
t.panel = Color32::from_rgb(39, 43, 52);
@@ -268,12 +310,12 @@ impl Tokens {
t.render_ink = Color32::from_rgb(224, 116, 174);
t.render_soft = Color32::from_rgb(64, 35, 54);
t.tab_on_fill_dark = Color32::BLACK;
t.dialog_body = Color32::from_rgb(59, 68, 83);
t.dialog_body = Color32::from_rgb(39, 43, 52);
t.dialog_footer = Color32::from_rgb(47, 52, 62);
t.dialog_rule = Color32::from_rgb(53, 59, 71);
t.control_fill = Color32::from_rgb(39, 43, 52);
t.control_fill = Color32::from_rgb(28, 31, 38);
t.control_border = Color32::from_rgb(74, 82, 98);
t.dialog_label = Color32::from_rgb(180, 187, 200);
t.dialog_label = t.text;
t.selected_ink = Color32::from_rgb(156, 199, 245);
t.selected_bg = Color32::from_rgb(42, 70, 110);
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
@@ -298,8 +340,8 @@ impl Tokens {
t.dimension_driven = Color32::from_rgb(142, 150, 164);
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
t.dialog_bg = Color32::from_rgb(59, 68, 83);
t.dialog_border = Color32::from_rgb(72, 83, 99);
t.dialog_bg = Color32::from_rgb(39, 43, 52);
t.dialog_border = Color32::from_rgb(63, 69, 82);
t.chip = Color32::from_rgb(59, 101, 168);
t.glyph_bg = Color32::from_rgb(44, 50, 60);
t.glyph_edge = Color32::from_rgb(200, 130, 120);
@@ -315,7 +357,7 @@ impl Tokens {
t.bend_up = Color32::from_rgb(110, 200, 120);
t.bend_down = Color32::from_rgb(240, 150, 80);
t.form_ink = t.text;
t.form_dim = t.text_dim;
t.form_dim = Color32::from_rgb(180, 187, 200);
t.form_primary = t.accent;
t.form_error = t.error;
t.form_error_fill = t.timeline_item_error;
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#ffad38",
"#eda134",
"#855a1d",
"#ffad38"
"#855a1d"
],
"manipulator": "#ffad38",
"mesh_accent": "#d6922f",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#d8f4ff",
"#8edcf5",
"#4386ac",
"#eef8ff"
"#4386ac"
],
"manipulator": "#66ddff",
"mesh_accent": "#78c6e8",
@@ -126,8 +126,7 @@
"logo": [
"#cba6f7",
"#477dc8",
"#586382",
"#cdd6f4"
"#586382"
],
"manipulator": "#cba6f7",
"mesh_accent": "#cba6f7",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#85b9ef",
"#4096e9",
"#2b71be",
"#ffffff"
"#2b71be"
],
"manipulator": "#007aff",
"mesh_accent": "#927300",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#4aa3d8",
"#2878a9",
"#68717d",
"#e0e2e5"
"#68717d"
],
"manipulator": "#4aa3d8",
"mesh_accent": "#4aa3d8",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#b3ffc4",
"#79ed95",
"#34854b",
"#d8ffdf"
"#34854b"
],
"manipulator": "#4dff79",
"mesh_accent": "#59d67b",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#00bfff",
"#0070d0",
"#555555",
"#ffffff"
"#555555"
],
"manipulator": "#00bfff",
"mesh_accent": "#00bfff",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#88c0d0",
"#5e81ac",
"#657991",
"#eceff4"
"#657991"
],
"manipulator": "#88c0d0",
"mesh_accent": "#88c0d0",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#bd93f9",
"#8456b8",
"#656581",
"#f8f8f2"
"#656581"
],
"manipulator": "#bd93f9",
"mesh_accent": "#bd93f9",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#61afef",
"#56b6c2",
"#c678dd",
"#abb2bf"
"#c678dd"
],
"manipulator": "#61afef",
"mesh_accent": "#857100",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#ccccff",
"#9999cc",
"#666699",
"#ffffff"
"#666699"
],
"manipulator": "#555599",
"mesh_accent": "#666699",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#268bd2",
"#346e88",
"#b5b6a0",
"#586e75"
"#b5b6a0"
],
"manipulator": "#268bd2",
"mesh_accent": "#268bd2",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#ffffff",
"#c0c0c0",
"#808080",
"#ffffff"
"#808080"
],
"manipulator": "#000080",
"mesh_accent": "#000080",
+1 -2
View File
@@ -126,8 +126,7 @@
"logo": [
"#7aa2f7",
"#4775ce",
"#4f6089",
"#c0caf5"
"#4f6089"
],
"manipulator": "#7aa2f7",
"mesh_accent": "#7aa2f7",
+3 -3
View File
@@ -4,7 +4,7 @@
use serde::{Deserialize, Serialize};
use crate::{DocError, Document, Result};
use crate::{DEFAULT_MATERIAL, DocError, Document, Result};
#[path = "config_rules.rs"]
mod rules;
@@ -96,7 +96,7 @@ impl Document {
/// returning None for the default material.
pub fn material_cell(&self, value: &str) -> Result<Option<String>> {
let name = self.material_name(value).ok_or_else(|| DocError::Invalid(format!("unknown configuration material `{value}`")))?;
Ok((name != "Default").then_some(name))
Ok((name != DEFAULT_MATERIAL).then_some(name))
}
/// Freeze design-unit-sensitive configuration literals when changing display units.
@@ -140,7 +140,7 @@ impl Document {
}
}),
Column::Suppress(id) => self.feature(*id).map(|f| f.suppressed.to_string()),
Column::Material(body) => Some(self.materials.get(body).cloned().unwrap_or_else(|| "Default".into())),
Column::Material(body) => Some(self.materials.get(body).cloned().unwrap_or_else(|| DEFAULT_MATERIAL.into())),
Column::Appearance(body) => Some(self.appearances.bodies.get(body).and_then(|look| serde_json::to_string(look).ok()).unwrap_or_default()),
Column::ComponentAppearance(id) => {
Some(self.appearances.components.get(id).and_then(|look| serde_json::to_string(look).ok()).unwrap_or_default())
+87 -7
View File
@@ -368,6 +368,8 @@ pub enum AxisRef {
World { axis: String },
/// Any line in the sketch plane, in world coordinates.
Line { origin: Position, dir: Dir },
/// A construction axis feature, by name, where it is when the revolve is evaluated.
Construction { name: FeatureName },
}
/// Persistent cylindrical Emboss selection, authored in the feature component frame.
@@ -1854,14 +1856,16 @@ fn fair_keep_edges() -> bool {
true
}
/// Triangles of one imported mesh body (millimetres).
/// Triangles of one imported mesh body (millimetres). The arrays are shared by clones: every
/// edit keeps the design as it was for undo, and a scanned part's triangles (hundreds of
/// megabytes) are not copied for that.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MeshData {
pub name: String,
/// x, y, z per vertex.
pub positions: Vec<f64>,
pub positions: std::sync::Arc<[f64]>,
/// Three vertex indices per triangle.
pub triangles: Vec<u32>,
pub triangles: std::sync::Arc<[u32]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<[f32; 3]>,
}
@@ -2026,6 +2030,10 @@ pub enum PatternKind {
alignment: solidcraft_sketch::PatternAlignment,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
reversed: bool,
/// A construction axis the copies turn about, by name, followed when it moves;
/// `origin` and `axis` are where it was when the pattern was made.
#[serde(default, skip_serializing_if = "Option::is_none")]
axis_feature: Option<FeatureName>,
},
/// Copies along a chain of sketch curves: instance k moves by path(k·spacing) − path(0),
/// measured along the path from its start.
@@ -2411,6 +2419,12 @@ pub struct Document {
pub marker: Option<usize>,
#[serde(default)]
pub next_id: u64,
/// Body names the design used and must not use again: a name a `body.rename` freed (#4957).
/// The design's own counter starts over at every evaluation, so without these a design that
/// is evaluated again after a rename hands the freed name to the next body, and a script
/// that names a body finds another one.
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
pub reserved_body_names: std::collections::BTreeSet<String>,
/// Components below the root, as a tree (by parent).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<Component>,
@@ -2618,6 +2632,33 @@ impl Default for Document {
}
impl Document {
/// The next shared identity, including auxiliary lists written by older versions or
/// populated programmatically. Reading it does not reserve an id (used by joint previews).
pub fn next_item_id(&self) -> Result<u64> {
let a = &self.assembly;
let largest = self
.browser_groups
.iter()
.map(|g| g.id)
.chain(self.timeline_groups.iter().map(|g| g.id))
.chain(self.canvases.iter().map(|c| c.id))
.chain(a.joints.iter().map(|j| j.id))
.chain(a.contact_sets.iter().map(|c| c.id))
.chain(a.studies.iter().map(|s| s.id))
.chain(a.exploded.iter().map(|v| v.id))
.max()
.unwrap_or(0);
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
Ok(self.next_id.max(floor).max(1))
}
/// Reserve an identity in the saved counter. Removing an item never rewinds this counter.
pub fn allocate_id(&mut self) -> Result<u64> {
let id = self.next_item_id()?;
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
Ok(id)
}
pub fn new(name: &str) -> Self {
Document {
render: Default::default(),
@@ -2630,6 +2671,7 @@ impl Document {
features: Vec::new(),
marker: None,
next_id: 1,
reserved_body_names: Default::default(),
components: Vec::new(),
occurrences: Vec::new(),
assembly: Default::default(),
@@ -2657,7 +2699,14 @@ impl Document {
/// Rename body `old` to `new` where the design keeps things by body name outside its
/// features: materials, appearances, configuration columns, offsets, components and the
/// browser's groups and order. (The features are the rename command's to change.)
///
/// A name the design numbered (`Body1`) is not used again after the rename: the design's
/// counter starts over at every evaluation, so without this a design that is evaluated
/// again hands the freed name to the next body (#4957).
pub fn rename_body_keys(&mut self, old: &str, new: &str) {
if old.strip_prefix("Body").and_then(|n| n.parse::<usize>().ok()).is_some() {
self.reserved_body_names.insert(old.to_string());
}
if let Some(m) = self.materials.remove(old) {
self.materials.insert(new.to_string(), m);
}
@@ -2807,9 +2856,21 @@ impl Document {
st
}
/// Density of a body's material (g/cm³).
/// The material a body's mass is computed with, as the library or this design spells it:
/// its assigned material when that is known and has a usable density, else
/// [`DEFAULT_MATERIAL`]. The assignment as stored (`materials`) is left as it is.
pub fn body_material(&self, body: &str) -> String {
self.materials
.get(body)
.and_then(|m| self.material_name(m))
.filter(|m| self.material_density(m).is_some())
.unwrap_or_else(|| DEFAULT_MATERIAL.into())
}
/// Density of a body's material (g/cm³): its [`Document::body_material`]'s, so the name a
/// body reads and the mass it has never disagree.
pub fn density(&self, body: &str) -> f64 {
self.materials.get(body).and_then(|m| self.material_density(m)).unwrap_or(DEFAULT_DENSITY)
self.material_density(&self.body_material(body)).unwrap_or(DEFAULT_DENSITY)
}
/// A material defined in this design, by name (case-insensitive).
@@ -3417,6 +3478,24 @@ impl Document {
Ok(())
}
/// Resolve circular Quantity expressions before solving. Rebuilding the copies here also
/// covers cold document evaluation and configuration changes, without an engine dependency.
pub fn apply_pattern_quantities(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
let counts: Vec<_> = sk
.patterns
.iter()
.filter_map(|p| p.count_expression.as_ref().map(|e| (p.id.clone(), e)))
.map(|(id, e)| {
let n = crate::eval::val(vals, e, Kind::Unitless)?;
Ok((id, solidcraft_sketch::SketchPattern::circular_quantity(n)?))
})
.collect::<Result<_>>()?;
for (id, count) in counts {
sk.resize_circular_pattern(&id, count)?;
}
Ok(())
}
/// Move the points placed by parameters to where their expressions come to (in place). Such
/// a point is fixed, so the solve keeps it there.
pub fn apply_point_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
@@ -3503,6 +3582,7 @@ impl Document {
moved_from: Option<&Sketch>,
analysis: solidcraft_sketch::Analysis,
) -> Result<solidcraft_sketch::SolveReport> {
self.apply_pattern_quantities(vals, sk)?;
self.apply_dimension_values(vals, sk)?;
let was = |sk: &Sketch| moved_from.and_then(|from| sk.positions_in(from));
let offsets = !sk.offsets.is_empty() || !sk.freeform_offsets.is_empty();
@@ -3523,10 +3603,10 @@ impl Document {
}
}
/// Density of the default material (g/cm³), as the default.
/// The built-in material library: (name, density), and each material's colour.
#[path = "../../../assets/materials/library.rs"]
mod materials;
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
pub use materials::{DEFAULT_DENSITY, DEFAULT_MATERIAL, MATERIALS, material_color};
/// A physical material defined in a design (`material.create`): its name, its density and,
/// optionally, the look it gives bodies without an appearance of their own.
+509 -99
View File
@@ -17,6 +17,8 @@ use crate::expr::{self, Kind, Value};
use crate::lineage::Traced;
use crate::{AxisRef, DocError, Document, Feature, FeatureKind, HoleKind, Operation, PlaneRef, ProfileSel, Result};
type JointFaces = (Body, kernel::Result<Arc<[kernel::FaceInfo]>>);
/// A body in the evaluated model.
#[derive(Clone, Debug)]
pub struct ModelBody {
@@ -31,6 +33,9 @@ pub struct ModelBody {
// Timeline snapshots are cloned before meshes are requested. Share the cell itself so
// meshing any snapshot also warms every unchanged copy of this body.
mesh: Arc<OnceLock<Arc<Mesh>>>,
/// Joint face measurements use their own tolerance, independent of display quality.
/// Shared by snapshots of this geometry; the body identity guards direct replacement.
joint_faces: Arc<OnceLock<JointFaces>>,
/// Lazy canonical reference triangles for a proper rigid occurrence placement.
mesh_placement: Option<Arc<placed_mesh::PlacedMesh>>,
/// The display plan (none when the body has no plan: a mesh body, or one the planner
@@ -71,6 +76,7 @@ impl ModelBody {
body,
feature,
mesh: Arc::new(OnceLock::new()),
joint_faces: Arc::new(OnceLock::new()),
mesh_placement: None,
plan: Arc::new(Default::default()),
plan_mesh: Arc::new(Default::default()),
@@ -86,6 +92,19 @@ impl ModelBody {
}
}
/// Face measurements for joint origins, at their original chord tolerance. Pose changes
/// and scratch copies share them; a geometry edit makes a new body and cache.
pub(crate) fn joint_faces(&self) -> Result<Arc<[kernel::FaceInfo]>> {
let measure = || {
#[cfg(test)]
cost_tests::count(|c| c.joint_faces += 1);
let tol = (self.body.size() * 2e-3).max(1e-3);
self.body.faces(tol).map(Arc::from)
};
let (body, faces) = self.joint_faces.get_or_init(|| (self.body.clone(), measure()));
if body.same(&self.body) { faces.clone().map_err(Into::into) } else { measure().map_err(Into::into) }
}
/// A box around the body without meshing it ([`Body::outline_bounds`]), worked out once.
pub fn outline_bounds(&self) -> solidcraft_geom::Aabb3 {
match self.outline.get_or_init(|| (self.body.clone(), self.body.outline_bounds())) {
@@ -120,6 +139,9 @@ impl ModelBody {
// finding their surface, picks naming their face, sketch planes), and a different
// triangulation moved those and changed what designs build. A GPU viewport draws the
// plan ([`ModelBody::display_plan`]).
if let Some(mesh) = self.mesh.get() {
return mesh.clone();
}
match self.mesh_placement.as_deref() {
Some(placed) => self.mesh.get_or_init(|| placed.mesh()).clone(),
None => placed_mesh::reference_mesh(&self.body, &self.mesh, self.display_tol()),
@@ -163,6 +185,50 @@ impl ModelBody {
.clone()
}
/// Identity of this body's display geometry, without meshing it: the cell its mesh is kept
/// in, shared by every clone of the body (timeline snapshots, world states that place it
/// unchanged) and, while it is held, by no other body.
pub fn display_identity(&self) -> Arc<dyn std::any::Any + Send + Sync> {
self.mesh.clone()
}
/// [`ModelBody::mesh`] as a shared mesh and the move that places it: for a rigid placement
/// of a definition, the definition's own mesh (the same one for every placement of it) and
/// the placement; for any other body its own mesh, unmoved. The mesh moved by the matrix is
/// `mesh()`, so a viewport can draw every placement of a part from one copy of its
/// triangles, without making the moved copies.
pub fn mesh_in_place(&self) -> (Arc<Mesh>, crate::Mat) {
match self.mesh_placement.as_deref() {
Some(placed) => placed.source_mesh(),
None => (self.mesh(), crate::IDENTITY),
}
}
/// The box of [`ModelBody::mesh`] without making a placement's moved copy: the shared mesh's
/// points, moved.
pub fn mesh_bounds(&self) -> solidcraft_geom::Aabb3 {
if let Some(mesh) = self.cached_mesh() {
return mesh.bounds();
}
let (mesh, m) = self.mesh_in_place();
if crate::is_identity(&m) {
return mesh.bounds();
}
mesh.positions.iter().fold(solidcraft_geom::Aabb3::EMPTY, |mut b, p| {
b.add(crate::apply_point(&m, *p));
b
})
}
/// [`ModelBody::plan_mesh_at`] as a shared mesh and the move that places it, as
/// [`ModelBody::mesh_in_place`] does for the reference mesh.
pub fn plan_mesh_in_place_at(&self, q: crate::display_quality::DisplayQuality) -> Option<(Arc<Mesh>, crate::Mat)> {
match self.mesh_placement.as_deref() {
Some(placed) => placed.source_plan_mesh(q, &self.name),
None => Some((self.plan_mesh_at(q)?, crate::IDENTITY)),
}
}
/// [`ModelBody::plan_mesh`] if it is made already; otherwise it is made on a worker thread
/// (once, however often this is asked) and `None` comes back meanwhile, so a caller drawing a
/// frame (a face highlighted under the pointer) is not held up by a large body's plan
@@ -650,6 +716,10 @@ pub struct ModelState {
/// Shared between states: a feature that does not change a sketch keeps it as it was.
pub sketches: Vec<Arc<SolvedSketch>>,
pub body_counter: usize,
/// Body names the design used and must not use again (a name a `body.rename` freed, #4957).
/// The counter starts over at every evaluation, so without these a design that is evaluated
/// again after a rename hands the freed name to the next body.
pub reserved_body_names: std::collections::BTreeSet<String>,
/// Sheet metal bodies (by body name) and their flat patterns.
pub sheets: Vec<crate::sheet::SheetBody>,
/// Construction planes, axes and points as evaluated (by feature).
@@ -685,6 +755,9 @@ impl ModelState {
self.sketch_keys.get(&s.feature).hash(&mut h);
}
self.body_counter.hash(&mut h);
for n in &self.reserved_body_names {
n.hash(&mut h);
}
for (feature, points) in &self.blend_edges {
feature.hash(&mut h);
for p in points {
@@ -950,6 +1023,11 @@ pub struct FeatureResult {
pub stale: bool,
#[serde(skip)]
fingerprint: u64,
/// Taken from a saved model that did not keep this feature's state (#3220): `state` only
/// stands in for it until [`Model::evaluate`] evaluates the feature again, which
/// [`Model::adopt`] does before the model is used.
#[serde(skip)]
unkept: bool,
#[serde(skip)]
pub state: Arc<ModelState>,
}
@@ -977,6 +1055,11 @@ pub struct Model {
kept: KeptSlot,
/// The imported texts' digests the last `evaluate` fingerprinted the features with.
step_digests: StepDigests,
/// The document context the last `evaluate` ran in ([`context_digest`]).
context: u64,
/// Results another copy of this model evaluated (ahead, off the window thread) that
/// `evaluate` takes for features they match ([`Model::prime`]), with that copy's context.
primed: Option<(u64, Arc<std::collections::HashMap<u64, FeatureResult>>)>,
}
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
@@ -1143,8 +1226,17 @@ fn evaluation_values(doc: &Document) -> (BTreeMap<String, Value>, BTreeMap<Strin
/// A feature's fingerprint in the chain after `prev`: its own inputs, where it is placed, and
/// the sheet metal or plastic rules it takes defaults from.
fn chain_fingerprint(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, prev: u64, rolled_back: bool, steps: &StepDigests) -> u64 {
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f);
#[allow(clippy::too_many_arguments)]
fn chain_fingerprint(
doc: &Document,
vals: &BTreeMap<String, Value>,
f: &Feature,
prev: u64,
rolled_back: bool,
steps: &StepDigests,
st: Option<&ModelState>,
) -> u64 {
let mut fp = fingerprint(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();
@@ -1229,9 +1321,157 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
h.finish()
}
/// Where components are placed, for a sketch that sees other components' geometry (on a face,
/// or with projections): it is resolved in its own component's view of them.
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
/// What a feature reads of placements: components (their first placement), occurrence paths
/// and bodies in its timeline input, whose geometry it finds in its evaluating frame, or all.
#[derive(Default)]
struct PlacementReads {
all: bool,
components: std::collections::BTreeSet<u64>,
paths: Vec<Vec<u64>>,
bodies: std::collections::BTreeSet<String>,
}
impl PlacementReads {
fn is_empty(&self) -> bool {
!self.all && self.components.is_empty() && self.paths.is_empty() && self.bodies.is_empty()
}
/// A body's complete placement in the feature's input state, including its offset. Later
/// transfers do not change where an earlier feature read it.
fn placed_body(&mut self, st: Option<&ModelState>, name: &str) {
if st.is_some_and(|st| st.body(name).is_some()) {
self.bodies.insert(name.to_owned());
} else {
self.all = true;
}
}
/// The same target selection as `apply_traced`, expanded only over the timeline input.
fn drawn_targets(&mut self, st: Option<&ModelState>, f: &Feature) {
let Some(st) = st else {
self.all = true;
return;
};
let (_, targets) = feature_op(f);
let names = if targets.is_empty() { &f.participants } else { &targets };
let all = names.len() == 1 && names.first().is_some_and(|n| n == "*");
if all || names.is_empty() {
self.bodies.extend(st.bodies.iter().filter(|b| all || b.component == f.component || b.feature == f.id).map(|b| b.name.clone()));
} else {
for name in names {
self.placed_body(Some(st), name);
}
}
}
/// The component of body `name` in `st`; every component when the body is not there (a
/// projection then takes the body nearest its point, wherever that is).
fn body(&mut self, doc: &Document, st: Option<&ModelState>, name: &str) {
match st.and_then(|st| st.body(name)) {
Some(b) => {
self.components.insert(doc.model_body_component(b));
}
None => self.all = true,
}
}
/// The component of sketch `id`, and the root when it holds wires fixed in the world (they
/// are read through the reader's whole placement).
fn sketch(&mut self, doc: &Document, id: u64) {
self.components.insert(doc.feature(id).map(|f| f.component).unwrap_or(0));
if doc.sketch(id).is_ok_and(|sk| sk.wires.iter().any(|w| w.space == solidcraft_sketch::WireSpace::World)) {
self.components.insert(0);
}
}
fn link(&mut self, doc: &Document, st: Option<&ModelState>, source: &solidcraft_sketch::LinkSource) {
use solidcraft_sketch::LinkSource as L;
match source {
L::Edge { body, .. } | L::Face { body, .. } | L::Body { body } | L::Vertex { body, .. } | L::Iso { body, .. } | L::Spun { body, .. } => {
self.body(doc, st, body)
}
L::SketchCurve { sketch, .. } | L::SketchPoint { sketch, .. } => self.sketch(doc, *sketch),
L::OnSurface { sketch, body, .. } => {
self.sketch(doc, *sketch);
self.body(doc, st, body);
}
L::Intersection { a, b } => {
self.link(doc, st, a);
self.link(doc, st, b);
}
// In the sketch's own frame: the component's origin, axes and planes, its own axes, text.
L::Origin | L::Axis { .. } | L::SketchAxis { .. } | L::Plane { .. } | L::Text { .. } => {}
}
}
}
/// The frame of an occurrence path from the root (its occurrences' transforms in turn); `None`
/// when the path no longer leads anywhere.
fn path_frame(doc: &Document, path: &[u64]) -> Option<Mat> {
if path.len() > crate::MAX_PLACEMENT_DEPTH + 1 {
return None;
}
let (mut m, mut c) = (crate::IDENTITY, 0);
for id in path {
let o = doc.occurrences.iter().find(|o| o.id == *id && o.parent == c)?;
m = crate::mat_mul(&m, &o.transform);
c = o.component;
}
Some(m)
}
/// Hash a placement on a grid far finer than anything a design shows ([`QUANTUM`] mm, a
/// thousandth of that for the rotation), so the same relative placement reached through
/// different whole-assembly motions (each rounding a little differently) reads the same.
fn hash_placement(m: &Mat, h: &mut impl Hasher) {
for (col, column) in m.iter().enumerate() {
let q = if col == 3 { QUANTUM } else { QUANTUM * 1e-3 };
for v in column {
let r = (v / q).round();
// -0 and 0 are the same place.
(if r == 0.0 { 0.0f64 } else { r }).to_bits().hash(h);
}
}
}
/// Where the geometry a feature reads from other components is placed, as its own component
/// sees it: the placement of each component (or occurrence path) it reads relative to its own
/// frame. A feature that reads nothing outside its own frame gives 0, so moving parts around
/// (a joint driven, an occurrence moved) leaves it as it was; one that reads a part moved
/// relative to it (a sketch projecting a face of another part, To Object, a mirror of a placed
/// body) is evaluated again. `st` is the model the feature starts from, which says what
/// component a body it names belongs to (without it, every component is taken as read).
/// [`placement_digest`] of a mirror of placed bodies: its own component's frame, each placement it
/// was picked at (the copy's component, frame and the frame it was picked in) and the offsets of
/// the bodies it mirrors (#1938).
fn mirror_placements_digest(doc: &Document, f: &Feature, placements: &[Vec<u64>], bodies: &[crate::BodyName]) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
placed.is_some().hash(&mut h);
if let Some((component, frame, picked)) = placed {
component.hash(&mut h);
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
}
}
for b in bodies {
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
}
h.finish()
}
fn placement_digest(doc: &Document, f: &Feature, st: Option<&ModelState>) -> u64 {
// No shortcut for a design without placements: its first component (placed where the
// design is) must not change what its features read, or creating it evaluates them all.
let mut reads = PlacementReads::default();
let mut frame = None;
let mut h = std::collections::hash_map::DefaultHasher::new();
// A sketch, profile or path of another component is moved through both placements.
for (sketch, _) in f.kind.profile_sels() {
if doc.feature(sketch).is_some_and(|s| s.component != f.component) {
reads.sketch(doc, sketch);
}
}
let path = match &f.kind {
FeatureKind::Sweep { path_sketch, .. }
| FeatureKind::SurfaceSweep { path_sketch, .. }
@@ -1248,10 +1488,8 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
_ => &[],
};
if path.as_ref().is_some_and(wired) || rails.iter().any(|r| wired(&r.sketch)) {
let mut hash = std::collections::hash_map::DefaultHasher::new();
let owner = path.and_then(|path| doc.feature(path.0)).map(|feature| feature.component).unwrap_or(0);
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
if let Some(path) = path {
reads.sketch(doc, path.0);
}
// A sweep's profile and the rails' components place them too.
let profile = match &f.kind {
@@ -1259,69 +1497,113 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
_ => None,
};
for sketch in profile.into_iter().chain(rails.iter().map(|r| &r.sketch)) {
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
reads.sketch(doc, sketch.0);
}
return hash.finish();
}
// A network through 3D sketch curves reads their points through the components the
// feature and those sketches are placed in, as a sweep's wired path does.
if let FeatureKind::SurfaceNetwork { u, v, .. } = &f.kind {
let sketches: Vec<crate::SketchId> = u.iter().chain(v).filter_map(|c| c.sketch).collect();
if sketches.iter().any(wired) {
let mut hash = std::collections::hash_map::DefaultHasher::new();
doc.component_transform(f.component).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
for sketch in &sketches {
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
return hash.finish();
}
}
// To / From Object finds its target where that target's component is placed (#928).
let to_object = match &f.kind {
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
FeatureKind::Revolve { to, .. } => to.is_some(),
_ => false,
};
// A mirror of placed bodies follows the placements it was picked at and its own component's
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
if let FeatureKind::Mirror { placements, bodies, .. } = &f.kind
&& !placements.is_empty()
{
let mut h = std::collections::hash_map::DefaultHasher::new();
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
placed.is_some().hash(&mut h);
if let Some((component, frame, picked)) = placed {
component.hash(&mut h);
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
reads.sketch(doc, sketch.0);
}
}
for b in bodies {
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
}
match &f.kind {
// To / From Object finds its target where that target's component is placed (#928),
// over every body.
FeatureKind::Extrude { extent, .. } if extent.to.is_some() || extent.from.is_some() => reads.all = true,
FeatureKind::Revolve { to: Some(_), .. } => reads.all = true,
// A mirror of placed bodies follows the placements it was picked at and its own component's
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
FeatureKind::Mirror { placements, bodies, .. } if !placements.is_empty() => return mirror_placements_digest(doc, f, placements, bodies),
// A combine moves tools placed differently from its target into the target's frame (#3383).
FeatureKind::Combine { target, tools, .. } => {
frame = st.and_then(|st| st.body(target)).map(|b| body_world(doc, b));
reads.placed_body(st, target);
for tool in tools {
reads.placed_body(st, tool);
}
}
return h.finish();
// Joins, cuts and intersects meet their targets where those are placed (#3383).
kind if drawn_tools_change_bodies(kind) => reads.drawn_targets(st, f),
FeatureKind::Sketch { plane, sketch } => {
match crate::face_anchor::plane_face_anchor(plane) {
// Read in the frame of the placement it was drawn in, its face through the
// placement it was picked at.
Some(anchor) => {
match anchor.author_frame(doc, f.component) {
Ok(m) => frame = Some(m),
Err(e) => e.to_string().hash(&mut h),
}
if anchor.path.is_empty() {
reads.components.insert(anchor.component);
} else {
reads.paths.push(anchor.path.clone());
}
}
// A face or edges picked on whatever body lies there.
None if plane_reads_geometry(plane) => reads.all = true,
None => {}
}
for link in &sketch.links {
reads.link(doc, st, &link.source);
}
}
_ => {}
}
// A combine moves tools placed differently from its target into the target's frame (#3383).
let combine = matches!(&f.kind, FeatureKind::Combine { .. });
if !to_object && !combine && drawn_tools_change_bodies(&f.kind) {
return drawn_tools_digest(doc, f);
}
if to_object || combine {
return if doc.occurrences.is_empty() && doc.body_offsets.is_empty() { 0 } else { placements_hash(doc) };
}
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
let anchor = crate::face_anchor::plane_face_anchor(plane);
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|| (!matches!(plane, PlaneRef::Face { .. }) && anchor.is_none() && sketch.links.is_empty())
{
if reads.is_empty() {
return 0;
}
placements_hash(doc)
let frame = frame.unwrap_or_else(|| doc.component_transform(f.component));
let Some(inverse) = crate::mat_inverse(&frame) else { return placements_hash(doc) };
if reads.all {
reads.components.insert(0);
match st {
// The components holding what the feature can reach: a component made later, or one
// that holds nothing yet (just created), changes nothing it reads.
Some(st) => {
reads.components.extend(doc.body_owners(st));
let owner = |id: u64| doc.feature(id).map(|g| g.component);
reads.components.extend(st.sketches.iter().filter_map(|s| owner(s.feature)));
reads.components.extend(st.construct.iter().filter_map(|c| owner(c.feature)));
}
None => reads.components.extend(doc.components.iter().map(|c| c.id)),
}
}
for c in &reads.components {
c.hash(&mut h);
// Its own component is read in its own frame.
if *c != f.component {
hash_placement(&crate::mat_mul(&inverse, &doc.component_transform(*c)), &mut h);
}
}
for path in &reads.paths {
path.hash(&mut h);
match path_frame(doc, path) {
Some(m) => hash_placement(&crate::mat_mul(&inverse, &m), &mut h),
None => u8::MAX.hash(&mut h),
}
}
for name in &reads.bodies {
name.hash(&mut h);
match st.and_then(|st| st.body(name)) {
Some(b) => hash_placement(&crate::mat_mul(&inverse, &body_world(doc, b)), &mut h),
None => u8::MAX.hash(&mut h),
}
}
// Legacy and component-wide reads still need the conservative offset dependency. Body
// reads above already include just their own offsets, in the frame that evaluates them.
if reads.all || !reads.components.is_empty() || !reads.paths.is_empty() {
serde_json::to_string(&doc.body_offsets).unwrap_or_default().hash(&mut h);
}
// Never 0: the feature reads placements.
h.finish() | 1
}
/// Every occurrence's placement and every body offset.
/// Every occurrence's placement and every body offset (a feature read from a frame that cannot
/// be inverted).
fn placements_hash(doc: &Document) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
for o in &doc.occurrences {
@@ -1419,7 +1701,8 @@ impl Model {
let marker = doc.marker.unwrap_or(usize::MAX);
let mut prev_fp = first.checked_sub(1).and_then(|i| self.results.get(i)).map_or(0, |r| r.fingerprint);
for (i, f) in doc.features.iter().enumerate().skip(first) {
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps);
let before = self.state_before(f.id);
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps, Some(&before));
if let Some(r) = self.results.get_mut(i).filter(|r| r.id == f.id) {
r.fingerprint = prev_fp;
}
@@ -1460,25 +1743,47 @@ impl Model {
let steps = StepDigests::of(doc, &self.step_digests);
let marker = doc.marker.unwrap_or(usize::MAX);
let mut prev = 0;
// The model each feature starts from (which components the bodies it names are in).
let mut state = self.empty.clone();
doc.features.iter().zip(&self.results).enumerate().all(|(i, (f, r))| {
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps);
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps, Some(&state));
if !r.skipped {
state = r.state.clone();
}
r.id == f.id && r.fingerprint == prev
})
}
/// Take the results `ahead` (a copy of this model, evaluated off this thread for the same
/// commands) computed: until [`Model::unprime`], `evaluate` reuses each one whose feature
/// has the same inputs here, instead of evaluating it again. Results that don't match are
/// ignored, so a design changed meanwhile is evaluated as usual.
pub fn prime(&mut self, ahead: &Model) {
let results = ahead.results.iter().filter(|r| !r.skipped && !r.stale).map(|r| (r.id, r.clone())).collect();
self.primed = Some((ahead.context, Arc::new(results)));
}
/// Stop offering the results [`Model::prime`] took.
pub fn unprime(&mut self) {
self.primed = None;
}
/// Re-evaluate the document, reusing results up to the first changed feature.
pub fn evaluate(&mut self, doc: &Document) {
let (vals, perr) = evaluation_values(doc);
self.param_errors = perr;
let mut prev_fp = 0u64;
let mut state = self.empty.clone();
// The names the design must not use again travel with the state (#4957).
let mut state = std::sync::Arc::new(ModelState { reserved_body_names: doc.reserved_body_names.clone(), ..(*self.empty).clone() });
let mut out: Vec<FeatureResult> = Vec::with_capacity(doc.features.len());
let mut reuse = !self.no_cache;
let mut recomputed = 0;
let mut restored = 0;
let marker = doc.marker.unwrap_or(usize::MAX);
let ctx = context_digest(doc, &vals);
self.context = ctx;
self.step_digests = StepDigests::of(doc, &self.step_digests);
let primed = self.primed.as_ref().filter(|(context, _)| !self.no_cache && *context == ctx).map(|(_, results)| results.clone());
// Each feature's component, for `Document::legacy_body_component` on every body after
// every feature without searching the timeline for each (the first feature with an id).
let mut feature_component: std::collections::HashMap<u64, u64> = std::collections::HashMap::with_capacity(doc.features.len());
@@ -1490,13 +1795,15 @@ impl Model {
let rolled_back = i >= marker;
// Opening was cancelled: the rest is skipped and the result discarded.
let cancelled = watched.step(i);
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests);
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests, Some(&state));
prev_fp = fp;
let current = |old: &&FeatureResult| old.id == f.id && old.fingerprint == fp && !old.stale;
// A state the saved model did not keep is evaluated again from the one before it; the
// results after it that the model did keep still stand (#3220).
let unkept = reuse && self.results.get(i).filter(current).is_some_and(|old| old.unkept);
if reuse
&& let Some(old) = self.results.get(i)
&& old.id == f.id
&& old.fingerprint == fp
&& !old.stale
&& !unkept
&& let Some(old) = self.results.get(i).filter(current)
{
if !old.skipped {
state = old.state.clone();
@@ -1504,7 +1811,7 @@ impl Model {
out.push(old.clone());
continue;
}
reuse = false;
reuse = unkept && !cancelled;
if f.suppressed || rolled_back || cancelled {
out.push(FeatureResult {
id: f.id,
@@ -1516,12 +1823,13 @@ impl Model {
skipped: true,
stale: false,
fingerprint: fp,
unkept: false,
state: state.clone(),
});
continue;
}
// 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), ctx, &state);
let key = cache_key(f, fp, fingerprint(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)))
@@ -1533,6 +1841,8 @@ impl Model {
if c.error.is_none() {
state = c.state.clone();
}
// A kept result after a failed one would not be this evaluation's.
reuse &= c.error.is_none();
out.push(FeatureResult {
id: f.id,
name: f.name.clone(),
@@ -1543,13 +1853,27 @@ impl Model {
skipped: false,
stale: false,
fingerprint: fp,
unkept: false,
state: state.clone(),
});
continue;
}
// Evaluated ahead on these same inputs (the same chain of definitions from the start).
if let Some(r) = primed.as_ref().and_then(|p| p.get(&f.id)).filter(|r| r.fingerprint == fp && !r.stale && !r.skipped) {
restored += 1;
if r.error.is_none() {
state = r.state.clone();
}
reuse &= r.error.is_none();
out.push(FeatureResult { state: state.clone(), unkept: false, ..r.clone() });
continue;
}
recomputed += 1;
let t0 = now();
let mut next = (*state).clone();
// The names the design must not use again travel with the state (#4957): a state
// taken from an earlier evaluation may have been made before a rename freed one.
next.reserved_body_names = doc.reserved_body_names.clone();
for b in &mut next.bodies {
b.made = crate::lineage::Made::Before;
}
@@ -1673,6 +1997,7 @@ impl Model {
m.put(key, e);
}
}
reuse &= error.is_none();
out.push(FeatureResult {
id: f.id,
name: f.name.clone(),
@@ -1683,6 +2008,7 @@ impl Model {
skipped: false,
stale: false,
fingerprint: fp,
unkept: false,
state: state.clone(),
});
}
@@ -1727,6 +2053,8 @@ impl Model {
b.name = new.to_string();
b.names = b.names.renamed();
}
// The names the design must not use again travel with the state (#4957).
st.reserved_body_names = doc.reserved_body_names.clone();
Arc::new(st)
})
.clone();
@@ -1787,9 +2115,12 @@ impl Model {
let marker = doc.marker.unwrap_or(usize::MAX);
self.step_digests = StepDigests::of(doc, &self.step_digests);
let mut prev = 0;
// Each feature's fingerprint reads the model it starts from, as evaluating does.
let mut before = Arc::new(ModelState::default());
for (i, (r, f)) in self.results.iter_mut().zip(&doc.features).enumerate() {
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests);
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests, Some(&before));
r.fingerprint = prev;
before = r.state.clone();
}
}
}
@@ -1839,8 +2170,10 @@ pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region
solidcraft_sketch::without_text_counters(&ss.sketch, chosen).into_iter().map(|p| p.region).collect()
}
ProfileSel::Indices { indices } => {
// A design file may list a profile more than once; it is still one profile.
let mut seen = std::collections::BTreeSet::new();
let mut v = Vec::new();
for i in indices {
for i in indices.iter().filter(|i| seen.insert(**i)) {
v.push(ps.get(*i).ok_or_else(|| DocError::Unknown(format!("profile {i} of {}", ss.name)))?.region.clone());
}
v
@@ -1906,7 +2239,8 @@ fn new_name(state: &mut ModelState, f: &Feature, k: usize) -> String {
loop {
state.body_counter += 1;
let n = format!("Body{}", state.body_counter);
if state.body(&n).is_none() {
// A name a rename freed is not handed out again (#4957).
if state.body(&n).is_none() && !state.reserved_body_names.contains(&n) {
return n;
}
}
@@ -2093,6 +2427,33 @@ fn clear_tools_as_one(tools: Vec<Traced>) -> Vec<Traced> {
}
}
/// An intersect never deletes a body by accident (#5295): it is refused when nothing of a named
/// target would remain, when nothing of any target would remain, or when the tools meet a target
/// and the boolean still came out empty. Only an unnamed target the tools miss altogether is
/// removed, as an intersect with every body does (#5160).
pub(crate) fn empty_intersection(bodies: &[&str]) -> DocError {
let body = match bodies {
[one] => format!("`{one}`"),
[first, rest @ ..] => format!("`{first}` and {} more", rest.len()),
[] => "the target body".to_string(),
};
DocError::Invalid(format!("the intersection is empty: nothing of {body} would remain; check that the profile overlaps the body"))
}
/// The bodies a feature's boolean takes part in, and whether they were named rather than taken
/// by default (every body, or the feature's component).
pub(crate) fn boolean_scope(state: &ModelState, f: &Feature, targets: &[String]) -> (bool, Vec<String>) {
let effective = if targets.is_empty() { &f.participants } else { targets };
let all = effective.len() == 1 && effective.first().is_some_and(|t| t == "*");
let names = state
.bodies
.iter()
.filter(|b| all || if effective.is_empty() { b.component == f.component || b.feature == f.id } else { effective.contains(&b.name) })
.map(|b| b.name.clone())
.collect();
(!all && !effective.is_empty(), names)
}
/// [`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<()> {
let tools = if op == Operation::Intersect { intersection_tool_union(tools)? } else { tools };
@@ -2100,6 +2461,7 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
// of its component (and what it made itself): other components are left alone.
let targets: &[String] = if targets.is_empty() { &f.participants } else { targets };
let all = targets.len() == 1 && targets.first().is_some_and(|t| t == "*");
let named = !all && !targets.is_empty();
let target_idx: Vec<usize> = if all {
(0..state.bodies.len()).collect()
} else if targets.is_empty() {
@@ -2234,6 +2596,24 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
None => None,
};
}
if op == Operation::Intersect && acc.as_ref().is_none_or(|b| b.body.face_count() == 0) {
let Some(mb) = state.bodies.get(ti) else { continue };
// A tool that meets the body yet leaves nothing is a failed boolean, not an empty result.
let mut met = false;
if !named && !mb.body.is_mesh() {
for t in &tools {
if mb.body.meets(&tool_in(t, into)?.body)? {
met = true;
break;
}
}
}
if named || met {
return Err(empty_intersection(&[mb.name.as_str()]));
}
remove.push(ti);
continue;
}
match acc {
Some(b) => {
if let Some(mb) = state.bodies.get_mut(ti) {
@@ -2243,6 +2623,10 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
None => remove.push(ti),
}
}
if op == Operation::Intersect && target_idx.iter().all(|i| remove.contains(i)) {
let names: Vec<&str> = target_idx.iter().filter_map(|i| state.bodies.get(*i)).map(|b| b.name.as_str()).collect();
return Err(empty_intersection(&names));
}
let mut i = 0;
state.bodies.retain(|_| {
let keep = !remove.contains(&i);
@@ -3038,7 +3422,17 @@ fn blend_target(state: &ModelState, body: &Option<crate::BodyName>, edges: &[Vec
})
}
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
/// The revolve axis in the sketch's 2D coordinates (a point on it, its direction). `construct`
/// gives a construction axis, by name, in the sketch's 3D frame.
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef, construct: &dyn Fn(&str) -> Result<(Vec3, Vec3)>) -> Result<(Vec2, Vec2)> {
let in_plane = |origin: Vec3, dir: Vec3| {
let pl = &ss.plane;
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin).abs() > 1e-6 {
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
}
Ok((pl.to_local(origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
};
match axis {
AxisRef::SketchLine { curve } => {
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("sketch line `{curve}`")))?;
@@ -3068,17 +3462,37 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
}
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
}
AxisRef::Line { origin, dir } => {
let pl = &ss.plane;
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin.0).abs() > 1e-6 {
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
}
Ok((pl.to_local(origin.0), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
AxisRef::Line { origin, dir } => in_plane(origin.0, dir.0),
AxisRef::Construction { name } => {
let (origin, dir) = construct(name)?;
in_plane(origin, dir)
}
}
}
/// A construction axis `name` of the timeline so far, as evaluated, in the frame of component
/// `comp`: a point on it and its direction. Without a document (`doc` None) it is taken in its
/// own component's frame.
pub(crate) fn construction_axis_in(doc: Option<&Document>, st: &ModelState, name: &str, comp: u64) -> Result<(Vec3, Vec3)> {
let c = st
.construct
.iter()
.find(|c| c.name == name)
.ok_or_else(|| DocError::Unknown(format!("construction axis `{name}` (it must come earlier in the timeline)")))?;
let crate::construct::ConstructGeom::Axis { origin, dir } = &c.geom else {
return Err(DocError::Invalid(format!("`{name}` is not a construction axis")));
};
let Some(doc) = doc else { return Ok((*origin, *dir)) };
let owner = doc.feature(c.feature).map_or(0, |f| f.component);
if owner == comp || doc.occurrences.is_empty() {
return Ok((*origin, *dir));
}
let inverse =
crate::mat_inverse(&doc.component_transform(comp)).ok_or_else(|| DocError::Invalid("the feature's component frame is invalid".into()))?;
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
Ok((crate::apply_point(&relative, *origin), crate::apply_vector(&relative, *dir)))
}
/// Whether a drill from `p` along `d` reaches material: it starts into a body (`touches`), or,
/// when `ahead` is set, a body lies further along `d`. A spot exactly on a surface counts only
/// when the drill enters there, not when it leaves. Only the bodies in `scope` count.
@@ -4113,7 +4527,7 @@ fn feature_tools_with_constructor(
f.component,
)?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let (o, d) = revolve_axis(&ss, axis)?;
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
// To Object is in this feature's component frame: the model as seen from there.
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
// Where the tool meets its targets is measured where they are shown (#3383).
@@ -4704,6 +5118,7 @@ fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
FeatureKind::Pipe { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Coil { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Rib { .. } => (Operation::Join, Vec::new()),
_ => (Operation::NewBody, Vec::new()),
}
}
@@ -4897,7 +5312,11 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
out.push(m);
}
}
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed } => {
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed, axis_feature } => {
let (origin, axis) = match axis_feature {
Some(name) => construction_axis_in(Some(doc), st, name, component)?,
None => (origin.0, axis.0),
};
let n = count(c)?;
let total = val(vals, angle, Kind::Angle)?;
let full = (total.abs() - std::f64::consts::TAU).abs() < 1e-9;
@@ -4909,7 +5328,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
0.0
};
for i in 1..n {
out.push(rotation(origin.0, axis.0, step * alignment.multiplier(i, *reversed)));
out.push(rotation(origin, axis, step * alignment.multiplier(i, *reversed)));
}
}
}
@@ -5059,6 +5478,16 @@ fn replay(
);
}
}
} else if let FeatureKind::Rib { .. } = &src.kind {
// A rib is built again at each copy of its sketch, against the bodies there (#4593):
// its walls run until they meet the body, which may not lie the same way there.
for (k, m) in mats.iter().enumerate() {
for body in more::rib_tools(vals, st, src, Some(m))? {
// Each copy's faces are named as the rib's, with the instance.
let t = Traced::unnamed(body);
copies.push(Traced { lineage: t.lineage.map(|l| l.copied(src.id, k + 1)), body: t.body });
}
}
} else {
// Each copy's faces are named as the source feature's, with the instance.
let tools = feature_tools_with_constructor(doc, vals, src, st, &mut None)?;
@@ -6419,25 +6848,6 @@ impl Drop for ToolFrames {
}
}
/// [`placement_digest`] of a feature with drawn tools: the placements, when it names the bodies
/// it changes (they may lie in another component, where its tools meet them, #3383); nothing
/// otherwise, as it then changes only bodies of its own component.
fn drawn_tools_digest(doc: &Document, f: &Feature) -> u64 {
let targets = match &f.kind {
FeatureKind::Extrude { targets, .. }
| FeatureKind::Revolve { targets, .. }
| FeatureKind::Loft { targets, .. }
| FeatureKind::Sweep { targets, .. }
| FeatureKind::Pipe { targets, .. }
| FeatureKind::Coil { targets, .. } => !targets.is_empty(),
_ => false,
};
if (!targets && f.participants.is_empty()) || (doc.occurrences.is_empty() && doc.body_offsets.is_empty()) {
return 0;
}
placements_hash(doc)
}
/// The matrix taking the evaluated feature's drawn tools into body `name`'s frame (`None`: the
/// same frame).
fn tool_frame(name: &str) -> Option<Mat> {
+100 -2
View File
@@ -13,15 +13,16 @@ use solidcraft_sketch::Sketch;
use crate::*;
/// Work counted on this thread: bodies moved into a component's frame, STEP texts digested.
/// Work counted on this thread: bodies moved, STEP texts digested, joint faces measured.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(super) struct Counts {
pub moved: usize,
pub digested: usize,
pub joint_faces: usize,
}
thread_local! {
static COUNTS: Cell<Counts> = const { Cell::new(Counts { moved: 0, digested: 0 }) };
static COUNTS: Cell<Counts> = const { Cell::new(Counts { moved: 0, digested: 0, joint_faces: 0 }) };
}
pub(super) fn count(f: impl FnOnce(&mut Counts)) {
@@ -39,6 +40,103 @@ fn counted(f: impl FnOnce()) -> Counts {
COUNTS.with(Cell::get)
}
#[test]
fn joint_face_measurements_are_shared_across_motion_samples_and_snap_kinds() {
use crate::joints::{Joint, JointKind, JointOrigin, Snap, XRef};
let mut doc = Document::new("joint face work");
let part = doc.add_component(Some("Crank"), 0).unwrap();
let occurrence = doc.occurrences.iter().find(|o| o.component == part).unwrap().id;
let id = doc
.add_feature(
FeatureKind::Cylinder {
base: point_expr(Vec3::ZERO),
axis: Dir(Vec3::Z),
radius: "2".into(),
height: "10".into(),
operation: Operation::NewBody,
},
None,
)
.unwrap();
doc.feature_mut(id).unwrap().component = part;
let mut model = Model::new();
model.evaluate(&doc);
let state = model.state();
let pick = Vec3::new(0.0, 0.0, 10.0);
let face = joints::face_name_at(&doc, &state, part, pick, false).unwrap();
doc.assembly.joints.push(Joint {
id: 100,
name: "Pivot".into(),
kind: JointKind::Revolute,
a: JointOrigin { occurrence: 0, snap: Snap::Frame { origin: pick, z: Vec3::Z, x: Some(Vec3::X) } },
b: JointOrigin { occurrence, snap: Snap::FaceCenter { pick, face: None, x: Some(XRef::Dir(Vec3::X)) } },
values: vec![0.0],
limits: Vec::new(),
flip: true,
offset: 0.0,
angle: 0.0,
suppressed: false,
exprs: Default::default(),
});
// One body supplies both old nearest-face snaps and persistent named-face snaps.
// A scratch model shares its measurements, even though each sample changes the pose.
let snapshot = model.clone();
let work = counted(|| {
for name in [None, Some(face)] {
doc.assembly.joints[0].b.snap = Snap::FaceCenter { pick, face: name, x: Some(XRef::Dir(Vec3::X)) };
for k in 0..145 {
let angle = (k as f64 * 2.5).to_radians();
doc.assembly.joints[0].values[0] = angle;
let solution = joints::solve(&doc, &snapshot.state());
assert!(solution.errors.is_empty() && solution.conflicts.is_empty(), "{solution:?}");
let point = apply_point(&solution.world[&occurrence], Vec3::new(5.0, 0.0, 10.0));
assert!(point.dist(Vec3::new(5.0 * angle.cos(), 5.0 * angle.sin(), 10.0)) < 1e-9);
doc.apply_joint_solution(&solution);
}
}
});
assert_eq!(work.joint_faces, 1, "pose-only samples must not remeasure unchanged faces: {work:?}");
// Editing geometry creates a fresh measurement without changing a retained snapshot.
let FeatureKind::Cylinder { height, .. } = &mut doc.feature_mut(id).unwrap().kind else { panic!("cylinder") };
*height = "12".into();
model.evaluate(&doc);
let origin = &doc.assembly.joints[0].b;
let work = counted(|| {
let changed = joints::resolve_origin(&doc, &model.state(), origin).unwrap();
let retained = joints::resolve_origin(&doc, &snapshot.state(), origin).unwrap();
assert!((changed[3][2] - 12.0).abs() < 1e-9);
assert!((retained[3][2] - 10.0).abs() < 1e-9);
});
assert_eq!(work.joint_faces, 1, "only the edited body must be measured: {work:?}");
}
#[test]
fn joint_face_measurements_never_follow_a_replaced_body_or_stale_offset() {
use crate::joints::{JointOrigin, Snap};
let mut doc = Document::new("replaced joint geometry");
let mut state = ModelState::default();
state.bodies.push(ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 10.0).unwrap(), 0));
let origin = JointOrigin { occurrence: 0, snap: Snap::FaceCenter { pick: Vec3::new(0.0, 0.0, 10.0), face: None, x: None } };
let work = counted(|| {
joints::resolve_origin(&doc, &state, &origin).unwrap();
});
assert_eq!(work.joint_faces, 1);
let retained = state.clone();
// Body is public; a clone's old measurement must never mask a direct replacement.
state.bodies[0].body = kernel::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 12.0).unwrap();
let changed = joints::resolve_origin(&doc, &state, &origin).unwrap();
assert!((changed[3][2] - 12.0).abs() < 1e-9);
let work = counted(|| {
let old = joints::resolve_origin(&doc, &retained, &origin).unwrap();
assert!((old[3][2] - 10.0).abs() < 1e-9);
doc.body_offsets.insert("Pin".into(), rigid(Vec3::new(0.0, 0.0, 1.0), Vec3::ZERO, Vec3::Z, 0.0));
let moved = joints::resolve_origin(&doc, &retained, &origin).unwrap();
assert!((moved[3][2] - 11.0).abs() < 1e-9);
});
assert_eq!(work.joint_faces, 0, "offsets are applied after the shared face measurements");
}
/// `parts` imported files of two boxes each, every one in its own component placed away from
/// the origin (as inserted parts are).
fn placed_imports(parts: usize) -> Document {
+89 -41
View File
@@ -69,27 +69,8 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
}
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } => {
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let t = val(vals, thickness, Kind::Length)?;
if !(t > 0.0) {
return Err(DocError::Invalid("the rib thickness must be positive".into()));
}
let depth = match depth {
Some(e) => Some(val(vals, e, Kind::Length)?)
.filter(|d| *d > 0.0)
.ok_or_else(|| DocError::Invalid("the rib depth must be positive".into()))
.map(Some)?,
None => None,
};
let groups: Vec<Vec<String>> = if *web { curves.iter().map(|c| vec![c.clone()]).collect() } else { vec![curves.clone()] };
if groups.is_empty() || curves.is_empty() || curves.len() > 1000 {
return Err(DocError::Invalid("a rib needs 1…1000 sketch curves".into()));
}
let mut tools = Vec::new();
for g in &groups {
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
}
FeatureKind::Rib { .. } => {
let tools = rib_tools(vals, st, f, None)?;
apply_op(st, f, tools, Operation::Join, &[])
}
FeatureKind::ReplaceFace { faces, target, body } => {
@@ -176,6 +157,41 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
}
/// The walls of rib `f` (one per curve for a web), each run until it meets the bodies. `at`
/// moves the rib's sketch first: a pattern's or mirror's copy of the rib is built again where
/// the copy is, against the bodies there (#4593), as a hole is drilled again at each copy.
pub(super) fn rib_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, at: Option<&crate::Mat>) -> Result<Vec<Body>> {
let FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } = &f.kind else {
return Err(DocError::Invalid(format!("{} is not a rib", f.name)));
};
let mut ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
if let Some(m) = at {
let p = ss.plane;
ss.plane = Plane::new(apply_point(m, p.origin), crate::apply_vector(m, p.x), crate::apply_vector(m, p.y))
.ok_or_else(|| DocError::Invalid("the rib's sketch plane at the copy".into()))?;
}
let t = val(vals, thickness, Kind::Length)?;
if !(t > 0.0) {
return Err(DocError::Invalid("the rib thickness must be positive".into()));
}
let depth = match depth {
Some(e) => Some(val(vals, e, Kind::Length)?)
.filter(|d| *d > 0.0)
.ok_or_else(|| DocError::Invalid("the rib depth must be positive".into()))
.map(Some)?,
None => None,
};
let groups: Vec<Vec<String>> = if *web { curves.iter().map(|c| vec![c.clone()]).collect() } else { vec![curves.clone()] };
if groups.is_empty() || curves.is_empty() || curves.len() > 1000 {
return Err(DocError::Invalid("a rib needs 1…1000 sketch curves".into()));
}
let mut tools = Vec::new();
for g in &groups {
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
}
Ok(tools)
}
/// Unit outward normal of the planar face through `p`: of the planar faces whose plane runs
/// through it, the one nearest it. A face elsewhere in such a plane (the wall of a notch, the end
/// of a rib, in line with a rim) is no pick of `p`, though its plane runs through it as exactly.
@@ -728,7 +744,9 @@ pub(super) fn sheet_eval(
None => p.0,
});
}
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks)?;
let placement = doc.component_transform(doc.model_body_component(&mb));
let body_to_world = doc.model_body_offset(&mb).map(|offset| crate::mat_mul(&placement, offset)).unwrap_or(placement);
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks, &body_to_world)?;
for note in notes {
append_warning(warning, note);
}
@@ -1327,6 +1345,9 @@ fn sheet_prism_cut(
} else {
let material = sh.flat_material()?;
if material.is_empty() {
if operation == Operation::Intersect && super::boolean_scope(st, f, targets).0 {
return Err(super::empty_intersection(&[sh.body.as_str()]));
}
// Empty exact material removes geometry and metadata together. Budget or
// topology errors propagate; they are never mistaken for an empty blank.
plans.push((sh.body.clone(), None));
@@ -1338,6 +1359,14 @@ fn sheet_prism_cut(
}
}
}
if operation == Operation::Intersect {
// Flat material is exact: an empty one is a true miss, removed only while some target remains.
let (_, scope) = super::boolean_scope(st, f, targets);
let emptied: Vec<&str> = plans.iter().filter(|(_, r)| r.is_none()).map(|(name, _)| name.as_str()).collect();
if !emptied.is_empty() && scope.iter().all(|b| emptied.contains(&b.as_str())) {
return Err(super::empty_intersection(&emptied));
}
}
let handled = plans.iter().map(|(name, _)| name.clone()).collect();
if !notes.is_empty() {
append_warning(warning, notes.join("; "));
@@ -1714,6 +1743,21 @@ fn offset_left(p: &[Vec2], d: f64) -> Vec<Vec2> {
.collect()
}
/// The smallest size a plastic feature's lengths may have, in millimetres. Thinner walls and
/// shorter arms than this are below what the solid booleans can resolve.
const PLASTIC_MIN_SIZE: f64 = 0.01;
/// `v`, the feature's `what`, when it is at least [`PLASTIC_MIN_SIZE`].
fn plastic_size(feature: &str, what: &str, v: f64) -> Result<f64> {
if v.is_finite() && v >= PLASTIC_MIN_SIZE {
Ok(v)
} else {
// Tiny values in exponent form: 1e-320 written out is hundreds of zeros.
let got = if v != 0.0 && v.abs() < 1e-3 { format!("{v:e}") } else { format!("{v}") };
Err(DocError::Invalid(format!("the {feature}'s {what} must be at least {PLASTIC_MIN_SIZE} mm (got {got} mm)")))
}
}
pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
let len = |e: &str| val(vals, e, Kind::Length);
// The plastic rule of the body a feature goes on gives its defaults (draft, clearance).
@@ -1738,10 +1782,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
body,
} => {
let target = body_under(st, body, position.0)?;
let (r0, h) = (len(diameter)? / 2.0, len(height)?);
if !(r0 > 0.0 && h > 0.0) {
return Err(DocError::Invalid("the boss diameter and height must be positive".into()));
}
let r0 = plastic_size("boss", "diameter", len(diameter)?)? / 2.0;
let h = plastic_size("boss", "height", len(height)?)?;
let dr = match draft {
Some(e) => val(vals, e, Kind::Angle)?,
None => rule(&target)?.map_or(0.0, |r| r.draft),
@@ -1784,8 +1826,10 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
None => 0.0,
};
let hr = h - off;
if n > 0.0 && !(t > 0.0 && l > 0.0 && hr > 0.0) {
return Err(DocError::Invalid("ribs need a positive thickness, length and height".into()));
if n > 0.0 {
plastic_size("boss", "rib_thickness", t)?;
plastic_size("boss", "rib_length", l)?;
plastic_size("boss", "rib height (height less rib_offset)", hr)?;
}
for k in 0..n as usize {
let a = std::f64::consts::TAU * k as f64 / n;
@@ -1816,9 +1860,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
}
if let Some(hd) = hole_diameter {
let rh = len(hd)? / 2.0;
let rh = plastic_size("boss", "hole_diameter", len(hd)?)? / 2.0;
let depth = match hole_depth {
Some(e) => len(e)?,
Some(e) => plastic_size("boss", "hole_depth", len(e)?)?,
None => h,
};
if !(rh > 0.0 && rh < r_top && depth > 0.0) {
@@ -1835,14 +1879,15 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let target = body_under(st, body, face.0)?;
let mb = st.body(&target).cloned().ok_or_else(|| DocError::Invalid("body".into()))?;
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
let (w, h) = (len(width)?, len(height)?);
let what = if *groove { "groove" } else { "lip" };
let (w, h) = (plastic_size(what, "width", len(width)?)?, plastic_size(what, "height", len(height)?)?);
let g = match gap {
Some(e) => len(e)?,
None if *groove => rule(&target)?.map_or(0.0, |r| r.clearance),
None => 0.0,
};
if !(w > 0.0 && h > 0.0 && g >= 0.0) {
return Err(DocError::Invalid("the lip width and height must be positive".into()));
if !(g >= 0.0) {
return Err(DocError::Invalid("the groove's gap must not be negative".into()));
}
let plane = Plane::from_normal(face.0, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
let tol = (mb.body.size() * 1e-3).max(1e-3);
@@ -1920,9 +1965,11 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
let target = body_under(st, body, position.0)?;
let (l, t, w, cd, cl) = (len(length)?, len(thickness)?, len(width)?, len(catch_depth)?, len(catch_length)?);
if !(l > 0.0 && t > 0.0 && w > 0.0 && cd > 0.0 && cl > 0.0 && cl < l) {
return Err(DocError::Invalid("the snap fit's sizes must be positive and the catch shorter than the arm".into()));
let size = |what: &str, e: &str| plastic_size("snap fit", what, len(e)?);
let (l, t, w) = (size("length", length)?, size("thickness", thickness)?, size("width", width)?);
let (cd, cl) = (size("catch_depth", catch_depth)?, size("catch_length", catch_length)?);
if !(cl < l) {
return Err(DocError::Invalid("the snap fit's catch must be shorter than its arm".into()));
}
let (x, y, z) = local_frame(direction.0, Some(hook.0))?;
let delta = (l * 0.05).clamp(1e-3, 0.5);
@@ -1943,15 +1990,16 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, position.0)?;
let (w, h) = (len(width)?, len(height)?);
let l = length.as_deref().map(len).transpose()?;
let size = |what: &str, e: &str| plastic_size("rest", what, len(e)?);
let (w, h) = (size("width", width)?, size("height", height)?);
let l = length.as_deref().map(|e| size("length", e)).transpose()?;
let dr = match draft {
Some(e) => val(vals, e, Kind::Angle)?,
None => rule(&target)?.map_or(0.0, |r| r.draft),
};
let t = thickness.as_deref().map(len).transpose()?;
if !(w > 0.0 && h > 0.0 && l.is_none_or(|l| l > 0.0) && t.is_none_or(|t| t > 0.0 && 2.0 * t < w.min(l.unwrap_or(w)))) {
return Err(DocError::Invalid("the rest's sizes must be positive and its wall thinner than half its width".into()));
let t = thickness.as_deref().map(|e| size("thickness", e)).transpose()?;
if t.is_some_and(|t| 2.0 * t >= w.min(l.unwrap_or(w))) {
return Err(DocError::Invalid("the rest's wall must be thinner than half its width".into()));
}
if !(dr.abs() < 1.0) {
return Err(DocError::Invalid("the rest's draft must be under 57°".into()));
+1 -1
View File
@@ -304,7 +304,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let (o, d) = revolve_axis(&ss, axis)?;
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
let a = val(vals, angle, Kind::Angle)?;
let (start, sweep) = match (angle2, symmetric) {
(Some(a2), _) => {
+10 -4
View File
@@ -341,6 +341,12 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
let mut out = Vec::new();
// Parentheses open at this point: a comma outside them can only be a decimal comma.
let mut depth = 0usize;
// A single leading `=` marks the text as a formula (`=d3`, #4681) and is not part of it.
if let Some(k) = cs.iter().position(|(_, c)| !c.is_whitespace())
&& cs.get(k).is_some_and(|(_, c)| *c == '=')
{
i = k + 1;
}
while let Some(&(_, c)) = cs.get(i) {
if c.is_whitespace() {
i += 1;
@@ -564,7 +570,9 @@ fn is_name_shaped(s: &str) -> bool {
pub fn definition(s: &str) -> Option<(&str, &str)> {
let (name, rhs) = s.split_once('=')?;
let name = name.trim();
is_name_shaped(name).then(|| (name, rhs.trim()))
let rhs = rhs.trim();
// `w == 3` is not a definition: a second `=` would read as a formula marker (#4681).
(is_name_shaped(name) && !rhs.starts_with('=')).then_some((name, rhs))
}
/// The length unit of a design with these units: `mm` unless they name another length unit.
@@ -1252,8 +1260,6 @@ mod tests {
("szerokość = 20", "szerokość", "20"),
// Reserved names split too: the parameter check names the reason.
("mm = 5", "mm", "5"),
// A second `=` stays in the expression, which then fails to evaluate.
("w == 3", "w", "= 3"),
("w = a = 3", "w", "a = 3"),
// An empty expression is the caller's error to report.
("w =", "w", ""),
@@ -1262,7 +1268,7 @@ mod tests {
for (text, name, rhs) in splits {
assert_eq!(definition(text), Some((name, rhs)), "{text:?}");
}
for text in ["20", "20 mm", "width / 2", "= 20", "=", "", "2w = 3", "a b = 3", "a-b = 3", "w.x = 3", "(w) = 3", "-w = 3"] {
for text in ["20", "20 mm", "width / 2", "= 20", "=", "", "2w = 3", "a b = 3", "a-b = 3", "w.x = 3", "(w) = 3", "-w = 3", "w == 3"] {
assert_eq!(definition(text), None, "{text:?}");
}
}
+12
View File
@@ -28,6 +28,18 @@ impl FeatureKind {
v
}
/// Whether the feature names the feature `name` (the construction plane a plane is built on,
/// a patterned or mirrored feature).
pub fn names_feature(&self, name: &str) -> bool {
let mut found = false;
self.walk_refs(&mut |r| {
if let Ref::Feature(n) = r {
found |= n.as_ref() == name;
}
});
found
}
/// Point references to sketches (by id) and features (by name) at their copies, or at a
/// feature's new name (`timeline.rename`).
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
+86
View File
@@ -578,6 +578,7 @@ pub fn read(s: &str) -> Result<Document> {
validate_body_transfers(&v)?;
FORMATS.upgrade(&mut v, version, applied.as_deref())?;
let mut d: Document = serde_json::from_value(v).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
check_typed_numbers(&d)?;
let mut legacy_by_feature = std::collections::BTreeMap::<usize, Vec<usize>>::new();
for (feature, wire) in legacy_wires {
legacy_by_feature.entry(feature).or_default().push(wire);
@@ -674,6 +675,9 @@ pub fn read(s: &str) -> Result<Document> {
d.features.iter().map(|f| f.id).chain(d.components.iter().map(|c| c.id)).chain(d.occurrences.iter().map(|o| o.id)).max().unwrap_or(0);
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("damaged design: the id space is exhausted".into()))?;
d.next_id = d.next_id.max(floor).max(1);
// These lists used to allocate independently. Keep their existing identities, but never
// let a later deletion make them available again after loading an older design.
d.next_id = d.next_item_id()?;
d.format = format_string(FORMAT);
d.format_pending = pending_names();
Ok(d)
@@ -1043,6 +1047,16 @@ fn check_design_numbers(v: &Value) -> Result<()> {
}
}
/// The range check again, on the typed design: an integer such as `9223372036854775808` passes
/// [`check_numbers`] in the file (ids and counters are integers and may be large), but once it
/// lands in a coordinate or size it is a float, and floats are written as floats. Integer fields
/// (ids, counters, indexes) stay integers and are never limited, so the field's type decides
/// what is checked, not its name.
fn check_typed_numbers(d: &Document) -> Result<()> {
let typed = serde_json::to_value(d).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
check_design_numbers(&typed)
}
/// Every non-integer number in the file is finite and within ±`MAX_REAL` (a damaged or hostile
/// file is refused instead of overflowing the geometry later).
fn check_numbers(v: &Value, depth: usize) -> Result<()> {
@@ -2038,6 +2052,46 @@ mod tests {
}
}
#[test]
fn older_auxiliary_ids_advance_the_saved_counter_before_deletion() {
use serde_json::json;
let origin = json!({"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}});
let cases = [
("browser_groups", json!({"id": 100, "name": "Group", "component": 0, "folder": "bodies", "items": []})),
("timeline_groups", json!({"id": 100, "name": "Folder", "features": []})),
(
"canvases",
json!({"id": 100, "name": "Canvas", "format": "png", "data": "", "pixels": [1, 1],
"plane": {"type": "origin", "name": "XY"}, "center": [0, 0], "width": 10,
"angle": 0, "opacity": 0.5, "flip": false, "visible": true}),
),
("joints", json!({"id": 100, "name": "Joint", "kind": "rigid", "a": origin, "b": origin})),
("contact_sets", json!({"id": 100, "name": "Contact", "occurrences": []})),
("studies", json!({"id": 100, "name": "Study", "steps": 10, "keys": []})),
("exploded", json!({"id": 100, "name": "View", "moves": []})),
];
for (field, item) in cases {
let json = edited(&Document::new("Legacy IDs"), |v| {
v.as_object_mut().unwrap().remove("next_id");
if matches!(field, "browser_groups" | "timeline_groups" | "canvases") {
v[field] = json!([item]);
} else {
v["assembly"] = json!({field: [item]});
}
});
let mut back = read(&json).unwrap_or_else(|e| panic!("{field}: {e}"));
assert_eq!(back.next_id, 101, "{field}");
back.browser_groups.clear();
back.timeline_groups.clear();
back.canvases.clear();
back.assembly = Default::default();
let mut reopened = read(&back.to_json()).unwrap();
assert_eq!(reopened.allocate_id().unwrap(), 101, "{field}: deleted legacy id was forgotten");
assert_eq!(reopened.allocate_id().unwrap(), 102, "{field}");
}
}
#[test]
fn duplicate_component_or_occurrence_ids_are_a_damaged_design() {
let d = two_components_one_occurrence();
@@ -2052,6 +2106,38 @@ mod tests {
}
}
/// An integer-valued coordinate is range-checked like a float one, while ids and counters
/// may still be large integers (solidcraft#5021).
#[test]
fn integer_valued_coordinates_are_range_checked_and_large_ids_still_load() {
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_point(solidcraft_geom::Vec2::new(40.0, 0.0), None).unwrap();
let mut d = Document::new("range");
d.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
let last = |v: &Value| v["features"][0]["sketch"]["points"].as_array().map_or(0, |a| a.len() - 1);
for huge in ["9223372036854775808", "4294967295", "2000000000", "-2000000000"] {
let json = edited(&d, |v| {
let i = last(v);
v["features"][0]["sketch"]["points"][i]["pos"][1] = serde_json::from_str(huge).unwrap();
});
let error = read(&json).unwrap_err().to_string();
assert!(error.contains("damaged design") && error.contains("out of range"), "{huge}: {error}");
}
// In range, an integer-valued coordinate is fine.
let json = edited(&d, |v| {
let i = last(v);
v["features"][0]["sketch"]["points"][i]["pos"][1] = 1_000_000_000u64.into();
});
assert!(read(&json).is_ok());
// Ids and counters are integers and may be past the coordinate limit.
let json = edited(&d, |v| {
v["features"][0]["id"] = 5_000_000_000u64.into();
v["next_id"] = 5_000_000_001u64.into();
});
let back = read(&json).unwrap();
assert_eq!(back.features[0].id, 5_000_000_000);
}
#[test]
fn exhausted_id_space_is_an_error_not_a_panic() {
let d = two_components_one_occurrence();
@@ -0,0 +1,4 @@
//! A circular pattern can turn about a construction axis (`axis_feature`, its name) and a revolve
//! or surface revolve can take one as its axis (`"type": "construction"`, with `name`); both follow
//! the axis when it is edited. Older designs need no transformation: they name no construction
//! axis. The last release refuses a design saved with this (the newer format number).
@@ -0,0 +1,4 @@
//! Associative motion traces retain their occurrence path, point reference, joint bounds or
//! study identity, sample count and assembly branch seeds, alongside the generated sketch.
//! Older designs default to no associations: their saved curves remain fixed. Readers from
//! the last release refuse the newer format number instead of silently dropping associations.
@@ -0,0 +1,5 @@
//! The design saves the body names a `body.rename` freed (`Body1` after the body it named was
//! renamed), so a name the design used is not used again.
//!
//! A design saved before this has no such names: it keeps the names it has, and the next body
//! it makes may take a name an earlier rename freed.
@@ -0,0 +1,3 @@
//! Circular sketch patterns may retain a Quantity expression alongside their evaluated count.
//! Older designs have no expression and keep their fixed counts unchanged. The previous
//! release must request an update rather than discard the expression on save.
+291 -40
View File
@@ -572,6 +572,8 @@ pub struct Assembly {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub studies: Vec<crate::motion::MotionStudy>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub traces: Vec<crate::motion::MotionTrace>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exploded: Vec<crate::motion::ExplodedView>,
}
@@ -597,6 +599,15 @@ impl Assembly {
}
}
}
for trace in &self.traces {
if let crate::motion::TraceMotion::Joint { joint, from, to, .. } = &trace.motion {
for (label, value) in [("from", from), ("to", to)] {
if let serde_json::Value::String(e) = value {
out.push((*joint, format!("trace {} {label}", trace.sketch), e.as_str()));
}
}
}
}
out
}
@@ -616,6 +627,15 @@ impl Assembly {
}
}
}
for trace in &mut self.traces {
if let crate::motion::TraceMotion::Joint { from, to, .. } = &mut trace.motion {
for value in [from, to] {
if let serde_json::Value::String(e) = value {
out.push(e);
}
}
}
}
out
}
@@ -627,6 +647,7 @@ impl Assembly {
&& !self.contact_enabled
&& !self.contact_all
&& self.studies.is_empty()
&& self.traces.is_empty()
&& self.exploded.is_empty()
}
}
@@ -731,8 +752,7 @@ fn nearest_plane(doc: &Document, st: &ModelState, o: &JointOrigin, pick: Vec3) -
let mut best: Option<(f64, Vec3, Vec3)> = None;
for b in component_bodies(doc, st, comp) {
let offset = doc.model_body_offset(b);
let tol = (b.body.size() * 2e-3).max(1e-3);
for f in b.body.faces(tol)? {
for f in b.joint_faces()?.iter() {
let Some(n) = f.plane_normal else { continue };
let (c, n) = match offset {
Some(m) => (apply_point(m, f.centroid), apply_vector(m, n)),
@@ -836,6 +856,15 @@ pub fn clamp(j: &Joint, q: &mut [f64]) {
}
}
/// Work requested after placing occurrences and checking their joint constraints.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Analysis {
/// Placement, loop closure, conflicts and errors only; `Solution::dof` is empty.
Placement,
/// Also count each occurrence's remaining motion, including nearby singular poses.
DegreesOfFreedom,
}
/// The result of placing occurrences by their joints.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Solution {
@@ -852,7 +881,7 @@ pub struct Solution {
/// Remaining degrees of freedom per occurrence (6 = free, 0 = fixed): how many independent
/// values still move it, from the fixed occurrences, with loops kept closed and a value a
/// motion link sets moving with the values its link chain starts from (a driven gear has its
/// driving gear's one, a screw's travel its turn's).
/// driving gear's one, a screw's travel its turn's). Empty for [`Analysis::Placement`].
pub dof: BTreeMap<u64, usize>,
/// Occurrences fixed in place (grounded, or the root's).
pub fixed: Vec<u64>,
@@ -943,22 +972,36 @@ fn flip_of(j: &Joint) -> Mat {
if !j.flip { mat_mul(&m, &[[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) } else { m }
}
/// Component-space frames stay fixed throughout one solve. Cache their inverses without
/// regrouping products: reversed joints still invert the value-dependent link as before.
struct JointFrames {
a: Mat,
b: Mat,
inverse_a: Option<Mat>,
inverse_b: Option<Mat>,
}
impl JointFrames {
fn new(a: Mat, b: Mat) -> Self {
Self { a, b, inverse_a: mat_inverse(&a), inverse_b: mat_inverse(&b) }
}
fn through(&self, joint: &Joint, values: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
let link = mat_mul(&link_of(joint, values), self.inverse_b.as_ref()?);
if from_a { Some(mat_mul(&mat_mul(t, &self.a), &link)) } else { Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), self.inverse_a.as_ref()?)) }
}
}
/// B's world transform from A's (or A's from B's, going the other way) for the joint's values.
fn through(j: &Joint, (fa, fb): &(Mat, Mat), q: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
let link = mat_mul(&link_of(j, q), &mat_inverse(fb)?);
if from_a {
Some(mat_mul(&mat_mul(t, fa), &link))
} else {
// T_A = T_B · link⁻¹ · F_a⁻¹
Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), &mat_inverse(fa)?))
}
JointFrames::new(*fa, *fb).through(j, q, from_a, t)
}
/// The spanning tree the solver places occurrences along, out from the fixed ones.
struct Tree<'a> {
joints: BTreeMap<u64, &'a Joint>,
/// Joint frames in component coordinates (A's, B's).
frames: BTreeMap<u64, (Mat, Mat)>,
frames: BTreeMap<u64, JointFrames>,
fixed: BTreeMap<u64, Mat>,
/// Placed occurrence → (the occurrence it is placed from, the joint, from the joint's A side).
parent: BTreeMap<u64, (u64, u64, bool)>,
@@ -968,8 +1011,10 @@ struct Tree<'a> {
impl Tree<'_> {
fn step(&self, joint: u64, from_a: bool, t: &Mat, vals: &Values) -> Option<Mat> {
#[cfg(test)]
tests::PLACEMENT_STEPS.with(|steps| steps.set(steps.get() + 1));
let q = vals.get(&joint).map(Vec::as_slice).unwrap_or(&[]);
through(self.joints.get(&joint)?, self.frames.get(&joint)?, q, from_a, t)
self.frames.get(&joint)?.through(self.joints.get(&joint)?, q, from_a, t)
}
/// Where every placed occurrence is for these values.
@@ -1008,6 +1053,75 @@ impl Tree<'_> {
}
}
/// Storage shared by one residual callback's evaluations, scoped to its immutable tree.
/// Keep map nodes and path capacity, but invalidate every placement before reading new values.
struct PlacementWorkspace<'tree, 'doc> {
tree: &'tree Tree<'doc>,
placed: BTreeMap<u64, Option<Mat>>,
path: Vec<(u64, u64, bool)>,
}
impl<'tree, 'doc> PlacementWorkspace<'tree, 'doc> {
fn new(tree: &'tree Tree<'doc>) -> Self {
Self { tree, placed: BTreeMap::new(), path: Vec::new() }
}
fn at<'values>(&'values mut self, values: &'values Values) -> Placements<'values, 'tree, 'doc> {
for placed in self.placed.values_mut() {
*placed = None;
}
Placements { workspace: self, values }
}
}
/// Only this guard can read placements. Its values stay immutable until it is dropped;
/// obtaining the next guard clears validity while retaining the allocated storage.
struct Placements<'values, 'tree, 'doc> {
workspace: &'values mut PlacementWorkspace<'tree, 'doc>,
values: &'values Values,
}
impl Placements<'_, '_, '_> {
fn world(&mut self, mut occurrence: u64) -> Option<Mat> {
self.workspace.path.clear();
let mut t = loop {
if let Some(t) = self.workspace.placed.get(&occurrence).and_then(Option::as_ref).or_else(|| self.workspace.tree.fixed.get(&occurrence)) {
break *t;
}
if self.workspace.path.len() >= self.workspace.tree.order.len() {
return None;
}
let &(from, joint, from_a) = self.workspace.tree.parent.get(&occurrence)?;
self.workspace.path.push((occurrence, joint, from_a));
occurrence = from;
};
while let Some((occurrence, joint, from_a)) = self.workspace.path.pop() {
t = self.workspace.tree.step(joint, from_a, &t, self.values)?;
#[cfg(test)]
if !self.workspace.placed.contains_key(&occurrence) {
tests::PLACEMENT_INSERTS.with(|inserts| inserts.set(inserts.get() + 1));
}
self.workspace.placed.insert(occurrence, Some(t));
}
Some(t)
}
fn closure_gaps(&mut self, closing: &[u64], scale: f64, out: &mut Vec<f64>) -> Option<()> {
for id in closing {
let (joint, frames) = (self.workspace.tree.joints.get(id)?, self.workspace.tree.frames.get(id)?);
let tb = self.world(joint.b.occurrence)?;
let ta = self.world(joint.a.occurrence)?;
let want = self.workspace.tree.step(*id, true, &ta, self.values)?;
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
let at = apply_point(&frames.b, p);
let d = apply_point(&want, at) - apply_point(&tb, at);
out.extend([d.x, d.y, d.z]);
}
}
Some(())
}
}
/// Values the loop solver moves at most in one group of loops, and loop-closing joints it takes
/// on at most: larger ones are only checked.
const MAX_LOOP_VALUES: usize = 64;
@@ -1212,8 +1326,8 @@ fn loop_scale(tree: &Tree, around: &BTreeSet<u64>, vals: &Values) -> f64 {
let joints: Aabb3 = around
.iter()
.filter_map(|id| {
let (j, (fa, _)) = (tree.joints.get(id)?, tree.frames.get(id)?);
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, vals)?, fa), Vec3::ZERO))
let (j, frames) = (tree.joints.get(id)?, tree.frames.get(id)?);
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, vals)?, &frames.a), Vec3::ZERO))
})
.collect();
if joints.is_empty() { 1.0 } else { joints.diagonal().clamp(1.0, 1e6) }
@@ -1221,18 +1335,8 @@ fn loop_scale(tree: &Tree, around: &BTreeSet<u64>, vals: &Values) -> f64 {
/// How far each closing joint is from meeting: three points of B's joint frame, `scale` apart,
/// where the joint wants them against where the tree puts them (appended to `out`).
fn closure_gaps(tree: &Tree, closing: &[u64], vals: &Values, scale: f64, out: &mut Vec<f64>) -> Option<()> {
for id in closing {
let (j, (_, fb)) = (tree.joints.get(id)?, tree.frames.get(id)?);
let tb = tree.world(j.b.occurrence, vals)?;
let want = tree.step(*id, true, &tree.world(j.a.occurrence, vals)?, vals)?;
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
let at = apply_point(fb, p);
let d = apply_point(&want, at) - apply_point(&tb, at);
out.extend([d.x, d.y, d.z]);
}
}
Some(())
fn closure_gaps(workspace: &mut PlacementWorkspace, closing: &[u64], vals: &Values, scale: f64, out: &mut Vec<f64>) -> Option<()> {
workspace.at(vals).closure_gaps(closing, scale, out)
}
/// How far a closing joint may be off before it is a conflict: its frames apart by more than
@@ -1294,6 +1398,7 @@ fn nearby_pose(tree: &Tree, groups: &[Group], asm: &Assembly, linked: &BTreeSet<
(v + d * if k % 2 == 0 { w } else { -w }).max(*lo).min(*hi)
})
.collect();
let mut workspace = PlacementWorkspace::new(tree);
let mut work = near.clone();
let mut gaps = |x: &[f64]| -> Option<Vec<f64>> {
spent += 1;
@@ -1307,7 +1412,7 @@ fn nearby_pose(tree: &Tree, groups: &[Group], asm: &Assembly, linked: &BTreeSet<
}
apply_links(asm, &mut work);
let mut out = Vec::with_capacity(closing.len() * 9);
closure_gaps(tree, closing, &work, scale, &mut out)?;
closure_gaps(&mut workspace, closing, &work, scale, &mut out)?;
out.iter().all(|v| v.is_finite()).then_some(out)
};
if close_within(&mut gaps, &mut x, &bounds, (1e-10 * scale).min(1e-7), loose_gap(scale)).is_some()
@@ -1422,6 +1527,7 @@ fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &m
let (n, gaps) = (values.len(), closers.len() * 9);
// How many ways the occurrence moves at a pose, with the loops kept closed.
let mut free_at = |at: &Values| -> Option<usize> {
let mut workspace = PlacementWorkspace::new(tree);
let mut work = at.clone();
// The loops' gaps, then three points of the occurrence `scale` apart.
let mut f = |x: &[f64]| -> Option<Vec<f64>> {
@@ -1433,8 +1539,9 @@ fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &m
}
apply_links(asm, &mut work);
let mut out = Vec::with_capacity(closers.len() * 9 + 9);
closure_gaps(tree, &closers, &work, scale, &mut out)?;
let t = tree.world(*occ, &work)?;
let mut placed = workspace.at(&work);
placed.closure_gaps(&closers, scale, &mut out)?;
let t = placed.world(*occ)?;
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
let q = apply_point(&t, p);
out.extend([q.x, q.y, q.z]);
@@ -1530,6 +1637,7 @@ fn close_group(
if !moves.is_empty() {
first = first.min(1.0 / stride(&effective(&x0, 0.0), &effective(&x0, 1.0)));
}
let mut workspace = PlacementWorkspace::new(tree);
let mut work = vals.clone();
// How far each closing joint is from meeting with the held values `t` of the way along:
// three points of B's joint frame, where the joint wants them against where the tree puts them.
@@ -1540,7 +1648,7 @@ fn close_group(
}
place(&mut work, x, t);
let mut out = Vec::with_capacity(closing.len() * 9);
closure_gaps(tree, closing, &work, scale, &mut out)?;
closure_gaps(&mut workspace, closing, &work, scale, &mut out)?;
out.iter().all(|v| v.is_finite()).then_some(out)
};
// Already closed, with no held value moving. (A moving one is followed all the way: a
@@ -1690,6 +1798,13 @@ pub fn solve(doc: &Document, st: &ModelState) -> Solution {
/// of the other joints around the loop. The `held` values (the ones being driven) keep theirs,
/// moving there from the values given (where the loops last closed) so the others follow them.
pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
solve_holding_with_analysis(doc, st, held, Analysis::DegreesOfFreedom)
}
/// Place occurrences and check constraints with the requested diagnostic work. Placement-only
/// solves still close and check every loop, respect held values and limits, and report errors;
/// they leave `Solution::dof` empty rather than returning the spanning tree's approximate count.
pub fn solve_holding_with_analysis(doc: &Document, st: &ModelState, held: &Held, analysis: Analysis) -> Solution {
let asm = &doc.assembly;
let mut sol = Solution::default();
let mut vals = joint_values(asm);
@@ -1704,9 +1819,9 @@ pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
for id in &closing {
let Some(j) = tree.joints.get(id) else { continue };
let (Some(ta), Some(tb)) = (placed.get(&j.a.occurrence), placed.get(&j.b.occurrence)) else { continue };
let (Some(want), Some((_, fb))) = (tree.step(*id, true, ta, &vals), tree.frames.get(id)) else { continue };
let (Some(want), Some(frames)) = (tree.step(*id, true, ta, &vals), tree.frames.get(id)) else { continue };
// Measured at the joint, where the loop solver closes it.
let (dt, da) = mismatch(&mat_mul(&want, fb), &mat_mul(tb, fb));
let (dt, da) = mismatch(&mat_mul(&want, &frames.b), &mat_mul(tb, &frames.b));
if dt > CONFLICT_MM || da > CONFLICT_RAD {
sol.conflicts.push((
j.id,
@@ -1719,11 +1834,15 @@ pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
sol.conflicts.push((j.id, j.name.clone(), "the loop can't stay closed on the way to these values".into()));
}
}
if !closing.is_empty() {
loop_dof(&tree, &closing, asm, &vals, &mut dof);
}
for o in &doc.occurrences {
dof.entry(o.id).or_insert(6);
if analysis == Analysis::DegreesOfFreedom {
if !closing.is_empty() {
loop_dof(&tree, &closing, asm, &vals, &mut dof);
}
for o in &doc.occurrences {
dof.entry(o.id).or_insert(6);
}
} else {
dof.clear();
}
placed.remove(&0);
sol.world = placed;
@@ -1749,7 +1868,7 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
let joints: Vec<&Joint> = asm.joints.iter().filter(|j| !j.suppressed).take(MAX_JOINTS).collect();
// Joint frames in component coordinates (errors leave the joint out). An origin several joints
// share (a panel face every part sits on) is found once.
let mut frames: BTreeMap<u64, (Mat, Mat)> = BTreeMap::new();
let mut frames: BTreeMap<u64, JointFrames> = BTreeMap::new();
let mut found: BTreeMap<(u64, String), std::result::Result<Mat, String>> = BTreeMap::new();
let mut resolve = |o: &JointOrigin| -> Result<Mat> {
let Ok(comp) = component_of(doc, o.occurrence) else { return resolve_origin(doc, st, o) };
@@ -1770,7 +1889,7 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
}
match (resolve(&j.a), resolve(&j.b)) {
(Ok(fa), Ok(fb)) => {
frames.insert(j.id, (fa, fb));
frames.insert(j.id, JointFrames::new(fa, fb));
}
(Err(e), _) | (_, Err(e)) => sol.errors.push((j.id, j.name.clone(), e.to_string())),
}
@@ -1953,6 +2072,138 @@ pub fn frame_of(o: Vec3, z: Vec3, x: Option<Vec3>) -> Option<Mat> {
mod tests {
use super::*;
thread_local! {
pub(super) static PLACEMENT_STEPS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
pub(super) static PLACEMENT_INSERTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(super) fn placement_tree(asm: &Assembly) -> Tree<'_> {
Tree {
joints: asm.joints.iter().map(|j| (j.id, j)).collect(),
frames: asm.joints.iter().map(|j| (j.id, JointFrames::new(mat_mul(&shift(3.0, 4.0, 5.0), &rot_y(0.3)), rot_x(-0.2)))).collect(),
fixed: BTreeMap::from([(0, IDENTITY)]),
parent: BTreeMap::from([(1, (0, 1, true)), (2, (1, 2, false)), (3, (2, 3, true)), (4, (1, 4, true)), (99, (0, 5, true))]),
order: vec![1, 2, 3, 4, 99],
}
}
/// Shared ancestors are evaluated once, without visiting an unrelated assembly branch.
/// The same arithmetic must be used after values change and on a reversed tree edge.
#[test]
fn residual_placements_reuse_ancestors_only_for_the_current_values() {
let asm = chain(6, 0.2);
let tree = placement_tree(&asm);
let mut values = joint_values(&asm);
let expected: BTreeMap<u64, Mat> = [1, 2, 3, 4].into_iter().map(|id| (id, tree.world(id, &values).unwrap())).collect();
let mut workspace = PlacementWorkspace::new(&tree);
PLACEMENT_STEPS.with(|steps| steps.set(0));
{
let mut placed = workspace.at(&values);
for id in [3, 4, 3, 2, 4] {
assert_eq!(placed.world(id), expected.get(&id).copied());
}
assert_eq!(placed.world(0), Some(IDENTITY));
assert_eq!(placed.world(1234), None);
}
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 4, "one step per needed occurrence; no unrelated branch");
values.get_mut(&1).unwrap()[0] += 0.1;
apply_links(&asm, &mut values);
let changed = tree.world(3, &values).unwrap();
assert_ne!(Some(&changed), expected.get(&3));
let mut placed = workspace.at(&values);
assert_eq!(placed.world(3), Some(changed), "a new residual evaluation cannot reuse the previous values' placements");
}
/// Cached static inverses retain the original multiplication order for every joint kind,
/// flip and traversal direction. A non-invertible frame remains a failed step.
#[test]
fn prepared_joint_frames_match_fresh_inverses() {
fn reference(j: &Joint, fa: &Mat, fb: &Mat, q: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
let link = mat_mul(&link_of(j, q), &mat_inverse(fb)?);
if from_a { Some(mat_mul(&mat_mul(t, fa), &link)) } else { Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), &mat_inverse(fa)?)) }
}
let mut joint = chain(1, 0.0).joints.remove(0);
let fa = mat_mul(&shift(3.0, -7.0, 11.0), &rot_y(0.3));
let fb = mat_mul(&shift(-2.0, 5.0, 13.0), &rot_x(-0.2));
let t = mat_mul(&shift(17.0, 19.0, -23.0), &rot_z(0.7));
let singular = [[0.0; 4]; 4];
for (a, b) in [(fa, fb), (singular, fb), (fa, singular)] {
let frames = JointFrames::new(a, b);
for kind in [
JointKind::Rigid,
JointKind::Revolute,
JointKind::Slider,
JointKind::Cylindrical,
JointKind::PinSlot,
JointKind::Planar,
JointKind::Ball,
JointKind::Parallel,
JointKind::Perpendicular,
JointKind::Angle,
JointKind::PointOnPlane,
JointKind::PointOnLine,
JointKind::LineOnPlane,
] {
joint.kind = kind;
joint.offset = 2.7;
joint.angle = -0.41;
for flip in [false, true] {
joint.flip = flip;
for angle in [0.0, 1e-6, -0.37, std::f64::consts::PI] {
let q: Vec<f64> = (0..kind.dofs().len()).map(|i| angle * (i + 1) as f64).collect();
for forward in [false, true] {
assert_eq!(
frames.through(&joint, &q, forward, &t),
reference(&joint, &a, &b, &q, forward, &t),
"{kind:?}, {flip}, {angle}, {forward}"
);
}
}
}
}
}
}
/// The loop solver's residual entry point reuses shared ancestors for each perturbation;
/// row order, duplicate rows, append semantics and missing-origin failure stay unchanged.
#[test]
fn closure_residuals_reuse_shared_ancestors_for_each_perturbation() {
let mut asm = chain(6, 0.2);
asm.joints[5].a.occurrence = 3;
asm.joints[5].b.occurrence = 4;
let tree = placement_tree(&asm);
let mut workspace = PlacementWorkspace::new(&tree);
let mut cold = true;
for start in [0.2, 0.2 + 1e-6, 0.2 - 1e-6, 1.3] {
let mut values = joint_values(&asm);
values.get_mut(&1).unwrap()[0] = start;
apply_links(&asm, &mut values);
let mut expected = vec![42.0];
for id in [6, 6] {
let joint = tree.joints.get(&id).unwrap();
let frames = tree.frames.get(&id).unwrap();
let a = tree.world(joint.a.occurrence, &values).unwrap();
let b = tree.world(joint.b.occurrence, &values).unwrap();
let want = through(joint, &(frames.a, frames.b), values.get(&id).unwrap(), true, &a).unwrap();
for point in [Vec3::ZERO, Vec3::X * 37.0, Vec3::Y * 37.0] {
let at = apply_point(&frames.b, point);
let d = apply_point(&want, at) - apply_point(&b, at);
expected.extend([d.x, d.y, d.z]);
}
}
PLACEMENT_STEPS.with(|steps| steps.set(0));
PLACEMENT_INSERTS.with(|inserts| inserts.set(0));
let mut actual = vec![42.0];
assert_eq!(closure_gaps(&mut workspace, &[6, 6], &values, 37.0, &mut actual), Some(()));
assert_eq!(actual, expected);
assert_eq!(PLACEMENT_INSERTS.with(|inserts| inserts.get()), if cold { 4 } else { 0 }, "reuse cache storage between perturbations");
cold = false;
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 6, "four tree placements and two closing-joint steps");
assert_eq!(closure_gaps(&mut workspace, &[u64::MAX], &values, 37.0, &mut actual), None);
assert_eq!(actual, expected, "an unresolved joint does not insert a zero residual");
}
}
fn close(a: &Mat, b: &Mat) -> bool {
let (t, r) = mismatch(a, b);
t < 1e-9 && r < 1e-9
@@ -1960,7 +2211,7 @@ mod tests {
/// Revolute joints 1…n (joint 1 at `start`, the others at 0) linked 1 → 2 → … → n with
/// ratio 2, the links stored from the end of the chain; joint 3 limited to ±50.
fn chain(n: u64, start: f64) -> Assembly {
pub(super) fn chain(n: u64, start: f64) -> Assembly {
let origin = JointOrigin { occurrence: 0, snap: Snap::Point { point: Vec3::ZERO, z: None, x: None } };
let joints = (1..=n)
.map(|id| Joint {
+1 -2
View File
@@ -64,8 +64,7 @@ impl NamedFace<'_> {
/// The face's centre and outward normal (the component's coordinates), when it is planar.
pub fn plane(&self) -> Result<Option<(Vec3, Vec3)>> {
let tol = (self.body.body.size() * 2e-3).max(1e-3);
let faces = self.body.body.faces(tol)?;
let faces = self.body.joint_faces()?;
Ok(faces.get(self.face).and_then(|f| f.plane_normal.map(|n| (apply_point(&self.offset, f.centroid), apply_vector(&self.offset, n)))))
}
}
+80 -14
View File
@@ -9,8 +9,8 @@ use solidcraft_geom::Vec3;
use solidcraft_sketch::linalg;
use super::{
Assembly, Held, MAX_LOOP_VALUES, MAX_LOOP_WORK, MAX_LOOPS, Solution, Tree, Values, apply_links, build, close_within, closure_gaps, jacobian,
joint_values, largest, least_squares, limit_bounds, link_chains, loop_groups, loop_scale, loose_gap, unknowns,
Assembly, Held, MAX_LOOP_VALUES, MAX_LOOP_WORK, MAX_LOOPS, Solution, Tree, Values, apply_links, build, close_within, jacobian, joint_values,
largest, least_squares, limit_bounds, link_chains, loop_groups, loop_scale, loose_gap, unknowns,
};
use crate::{Document, ModelState, apply_point, apply_vector, mat_mul};
@@ -133,8 +133,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
let lever = path
.iter()
.filter_map(|id| {
let (j, (fa, _)) = (tree.joints.get(id)?, tree.frames.get(id)?);
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, &vals)?, fa), Vec3::ZERO).dist(point?))
let (j, frames) = (tree.joints.get(id)?, tree.frames.get(id)?);
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, &vals)?, &frames.a), Vec3::ZERO).dist(point?))
})
.fold(0.0, f64::max);
let span = point.map_or(scale, |p| scale.max(p.dist(r.to)).max(lever));
@@ -155,7 +155,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
let mt = if r.axis.is_some() { 6 } else { 3 };
let spent = Cell::new(0usize);
let mut work = vals.clone();
let mut f = |x: &[f64]| misses(tree, asm, r, scale, &vars, &closers, x, &mut work, &spent);
let mut workspace = super::PlacementWorkspace::new(tree);
let mut f = |x: &[f64]| misses(&mut workspace, asm, r, scale, &vars, &closers, x, &mut work, &spent);
let mut x = x0.clone();
let miss = |res: &[f64]| res.iter().take(mt).map(|v| v * v).sum::<f64>().sqrt();
let before = f(&x0).map_or(f64::INFINITY, |r| miss(&r));
@@ -216,7 +217,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
if let Some(first) = closers.first() {
let k = drive.len();
let mut alone = vals.clone();
let mut g = |x: &[f64]| misses(tree, asm, r, scale, &drive, &[], x, &mut alone, &spent);
let mut alone_workspace = super::PlacementWorkspace::new(tree);
let mut g = |x: &[f64]| misses(&mut alone_workspace, asm, r, scale, &drive, &[], x, &mut alone, &spent);
let (mut xo, bo, co) = (x0.get(..k).unwrap_or_default().to_vec(), bounds.get(..k).unwrap_or_default(), cap.get(..k).unwrap_or_default());
// The loops left open, nothing to pace.
if let Some(ro) = approach(&mut g, &|_, _| 0.0, mt, &mut xo, bo, co, tol, loose_gap(scale))
@@ -249,7 +251,7 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
/// The target's misses with the values `vars` at `x`: the point's (mm), then the axis's (as mm
/// across the chain's `scale`), then the gaps of the loops `closers`, weighted.
fn misses(
tree: &Tree,
workspace: &mut super::PlacementWorkspace,
asm: &Assembly,
r: &Reach,
scale: f64,
@@ -269,9 +271,18 @@ fn misses(
}
}
apply_links(asm, work);
let mut out = target(tree, r, scale, work)?;
let n = out.len();
closure_gaps(tree, closers, work, scale, &mut out)?;
let tree = workspace.tree;
// An open chain has only one target placement to read. Loops share their ancestors with
// that target, but a single placement needs no residual-local map.
let mut out = if closers.is_empty() {
target(tree, r, scale, work)?
} else {
let mut placed = workspace.at(work);
let mut out = target_at(&placed.world(r.occurrence)?, r, scale);
placed.closure_gaps(closers, scale, &mut out)?;
out
};
let n = if r.axis.is_some() { 6 } else { 3 };
for v in out.iter_mut().skip(n) {
*v *= LOOP_WEIGHT;
}
@@ -280,14 +291,17 @@ fn misses(
/// The target's misses for these values: the point's (mm), then the axis's (as mm across `scale`).
fn target(tree: &Tree, r: &Reach, scale: f64, vals: &Values) -> Option<Vec<f64>> {
let t = tree.world(r.occurrence, vals)?;
let d = apply_point(&t, r.point) - r.to;
Some(target_at(&tree.world(r.occurrence, vals)?, r, scale))
}
fn target_at(t: &crate::Mat, r: &Reach, scale: f64) -> Vec<f64> {
let d = apply_point(t, r.point) - r.to;
let mut out = vec![d.x, d.y, d.z];
if let Some((a, z)) = r.axis {
let w = (apply_vector(&t, a) - z) * scale;
let w = (apply_vector(t, a) - z) * scale;
out.extend([w.x, w.y, w.z]);
}
Some(out)
out
}
/// How many steps apart the values `vars` at `a` and at `b` put the `paced` values (each with its
@@ -447,3 +461,55 @@ fn approach(
}
Some(r)
}
#[cfg(test)]
mod tests {
use super::super::tests::{PLACEMENT_INSERTS, PLACEMENT_STEPS, chain, placement_tree};
use super::*;
/// Exercise the IK callback itself: removing sharing at this call site must fail even if
/// the reusable placement helper still passes its own tests.
#[test]
fn ik_residual_reuses_target_and_loop_ancestors_at_the_call_site() {
let mut asm = chain(6, 0.2);
asm.joints[5].a.occurrence = 3;
asm.joints[5].b.occurrence = 4;
let tree = placement_tree(&asm);
let base = joint_values(&asm);
let mut workspace = super::super::PlacementWorkspace::new(&tree);
let mut cold = true;
for axis in [None, Some((Vec3::Y, Vec3::Z))] {
let r = Reach { occurrence: 3, point: Vec3::new(1.0, 2.0, 3.0), to: Vec3::new(7.0, 11.0, 13.0), axis, joints: None };
for x in [0.2, 0.2 + 1e-6, 0.2 - 1e-6, 1.3] {
let mut reference = base.clone();
reference.get_mut(&1).unwrap()[0] = x;
apply_links(&asm, &mut reference);
let mut expected = target(&tree, &r, 37.0, &reference).unwrap();
let n = expected.len();
super::super::closure_gaps(&mut super::super::PlacementWorkspace::new(&tree), &[6], &reference, 37.0, &mut expected).unwrap();
for v in expected.iter_mut().skip(n) {
*v *= LOOP_WEIGHT;
}
let mut work = base.clone();
let spent = Cell::new(0);
PLACEMENT_STEPS.with(|steps| steps.set(0));
PLACEMENT_INSERTS.with(|inserts| inserts.set(0));
let actual = misses(&mut workspace, &asm, &r, 37.0, &[(1, 0)], &[6], &[x], &mut work, &spent);
assert_eq!(actual, Some(expected));
assert_eq!(
PLACEMENT_INSERTS.with(|inserts| inserts.get()),
if cold { 4 } else { 0 },
"IK must retain cache storage across evaluations"
);
cold = false;
assert_eq!(spent.get(), 1, "reuse must not skip a finite-difference evaluation or bypass its work cap");
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 5, "four tree placements shared by the target and loop, plus the closing step");
}
let mut work = base.clone();
let spent = Cell::new(MAX_LOOP_WORK);
PLACEMENT_STEPS.with(|steps| steps.set(0));
assert!(misses(&mut workspace, &asm, &r, 37.0, &[(1, 0)], &[6], &[0.2], &mut work, &spent).is_none());
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 0);
}
}
}
+209 -22
View File
@@ -6,7 +6,8 @@
//! hold (one that later features pass through unchanged) is recorded once, as are its face names
//! and the solved sketches the states share. Face names are kept as worked out, not as the
//! history they were worked out from: a model taken from a file names every face as the model
//! that was saved did.
//! that was saved did. A file without room for every state keeps fewer
//! ([`SavedModel::thinned`]); opening evaluates the features whose state it left out again.
use super::*;
use crate::history_names::{FaceInfo, Names};
@@ -181,6 +182,7 @@ impl Writer {
let sketches = st.sketches.iter().map(|s| self.sketch(s)).collect::<Option<Vec<_>>>()?;
let r = json!({
"bodies": runs(&bodies), "sketches": runs(&sketches), "body_counter": st.body_counter,
"reserved_body_names": st.reserved_body_names.iter().collect::<Vec<_>>(),
"sheets": serde_json::to_value(&st.sheets).ok()?, "construct": serde_json::to_value(&st.construct).ok()?,
"threads": serde_json::to_value(&st.threads).ok()?,
"sketch_keys": st.sketch_keys.iter().map(|(f, k)| json!([f, hex(*k)])).collect::<Vec<_>>(),
@@ -320,6 +322,11 @@ impl SavedModel {
bodies: ids("bodies", model_bodies.len())?.into_iter().filter_map(|i| model_bodies.get(i).cloned()).collect(),
sketches: ids("sketches", sketches.len())?.into_iter().filter_map(|i| sketches.get(i).cloned()).collect(),
body_counter: index(s.get("body_counter"), usize::MAX)?,
reserved_body_names: s
.get("reserved_body_names")
.and_then(Json::as_array)
.map(|a| a.iter().filter_map(|n| n.as_str().map(str::to_string)).collect())
.unwrap_or_default(),
sheets: serde_json::from_value(field("sheets")?).map_err(invalid)?,
construct: serde_json::from_value(field("construct")?).map_err(invalid)?,
threads: serde_json::from_value(field("threads")?).map_err(invalid)?,
@@ -353,12 +360,163 @@ impl SavedModel {
None => Ok(Arc::new(ModelState::default())),
}
}
/// This model with fewer states, for a design file without room for all of them (#3220): the
/// final state (what the design shows, kept whatever it costs), then the others, the slowest
/// feature's first (the ones opening would take longest to evaluate again; of equal ones the
/// later first), each while what it adds still fits in `budget` bytes: the bodies no state kept
/// so far holds, and the records of the state, those bodies, their names and sketches.
/// `pieces[i]` is what keeping body `i` stores, as ids and bytes: a piece that several bodies
/// or states need counts once. A feature whose state is left out keeps none, and opening
/// evaluates it again ([`Model::adopt`]). With the model, the index here of each body it keeps,
/// and the bytes it counted: over `budget` when the final state alone is. `None` only for
/// records it cannot read.
pub fn thinned(&self, pieces: &[Vec<(usize, usize)>], budget: usize) -> Option<(SavedModel, Vec<usize>, usize)> {
let list = |key: &str| self.records.get(key).and_then(Json::as_array);
let (features, states, model_bodies) = (list("features")?, list("states")?, list("model_bodies")?);
let (names, sketches) = (list("names")?, list("sketches")?);
let at = |v: Option<&Json>, len: usize| v.and_then(Json::as_u64).and_then(|i| usize::try_from(i).ok()).filter(|i| *i < len);
// The indices a state's runs list.
let held = |s: &Json, key: &str, len: usize| -> Option<Vec<usize>> {
let mut out = Vec::new();
for run in s.get(key)?.as_array()? {
let first = at(run.get(0), len)?;
out.extend(first..first.checked_add(at(run.get(1), len.saturating_add(1))?).filter(|end| *end <= len)?);
}
Some(out)
};
let state_bodies = states.iter().map(|s| held(s, "bodies", model_bodies.len())).collect::<Option<Vec<_>>>()?;
let state_sketches = states.iter().map(|s| held(s, "sketches", sketches.len())).collect::<Option<Vec<_>>>()?;
let body_of = model_bodies.iter().map(|r| at(r.get("body"), self.bodies.len())).collect::<Option<Vec<_>>>()?;
let names_of = model_bodies.iter().map(|r| at(r.get("names"), names.len())).collect::<Option<Vec<_>>>()?;
// What each record takes in the file.
let bytes = |list: &[Json]| list.iter().map(|r| serde_json::to_vec(r).map_or(usize::MAX, |b| b.len().saturating_add(1))).collect::<Vec<_>>();
let (state_bytes, body_bytes) = (bytes(states.as_slice()), bytes(model_bodies.as_slice()));
let (name_bytes, sketch_bytes) = (bytes(names.as_slice()), bytes(sketches.as_slice()));
// What each state's feature took to evaluate, and the final state.
let mut ms = vec![0.0f64; states.len()];
let mut last = None;
for f in features.iter().filter(|f| f.get("skipped").and_then(Json::as_bool) == Some(false)) {
let s = at(f.get("state"), states.len())?;
if let Some(t) = ms.get_mut(s) {
*t += f.get("ms").and_then(Json::as_f64).filter(|t| t.is_finite() && *t > 0.0).unwrap_or(0.0);
}
last = Some(s);
}
let last = last?;
let time = |s: &usize| ms.get(*s).copied().unwrap_or(0.0);
let mut order: Vec<usize> = (0..states.len()).filter(|s| *s != last).collect();
order.sort_by(|a, b| time(b).total_cmp(&time(a)).then(b.cmp(a)));
let mut keep = vec![false; states.len()];
// The records and pieces kept so far, and their bytes.
let mut kept_bodies = vec![false; model_bodies.len()];
let (mut names_used, mut sketches_used) = (vec![false; names.len()], vec![false; sketches.len()]);
let mut stored = std::collections::HashSet::new();
let mut used = 0usize;
for s in std::iter::once(last).chain(order) {
let (mut adds, mut new_names, mut new_sketches) = (HashMap::new(), std::collections::HashSet::new(), std::collections::HashSet::new());
let mut records = *state_bytes.get(s)?;
for &m in state_bodies.get(s)? {
if kept_bodies.get(m) == Some(&false) {
records = records.saturating_add(*body_bytes.get(m)?);
let n = *names_of.get(m)?;
if names_used.get(n) == Some(&false) && new_names.insert(n) {
records = records.saturating_add(*name_bytes.get(n)?);
}
for &(piece, bytes) in pieces.get(*body_of.get(m)?)? {
if !stored.contains(&piece) {
adds.insert(piece, bytes);
}
}
}
}
for &k in state_sketches.get(s)? {
if sketches_used.get(k) == Some(&false) && new_sketches.insert(k) {
records = records.saturating_add(*sketch_bytes.get(k)?);
}
}
let total = adds.values().fold(used.saturating_add(records), |t, b| t.saturating_add(*b));
if total > budget && s != last {
continue;
}
used = total;
stored.extend(adds.into_keys());
for &m in state_bodies.get(s)? {
*kept_bodies.get_mut(m)? = true;
}
for n in new_names {
*names_used.get_mut(n)? = true;
}
for k in new_sketches {
*sketches_used.get_mut(k)? = true;
}
*keep.get_mut(s)? = true;
}
// What the states kept refer to, numbered again in the order it had.
let mut kernel_used = vec![false; self.bodies.len()];
for m in (0..model_bodies.len()).filter(|m| kept_bodies.get(*m) == Some(&true)) {
*kernel_used.get_mut(*body_of.get(m)?)? = true;
}
let renumber = |used: &[bool]| -> Vec<Option<usize>> {
let mut next = 0;
used.iter()
.map(|&u| {
u.then(|| {
next += 1;
next - 1
})
})
.collect()
};
let (new_body, new_kernel, new_names) = (renumber(&kept_bodies), renumber(&kernel_used), renumber(&names_used));
let (new_sketch, new_state) = (renumber(&sketches_used), renumber(&keep));
let new = |map: &[Option<usize>], i: usize| map.get(i).copied().flatten();
let kept_of = |list: &[Json], used: &[bool]| list.iter().zip(used).filter(|(_, u)| **u).map(|(v, _)| v.clone()).collect::<Vec<_>>();
let mut out_states = Vec::new();
for (s, st) in states.iter().enumerate().filter(|(s, _)| keep.get(*s) == Some(&true)) {
let bodies = state_bodies.get(s)?.iter().map(|m| new(&new_body, *m)).collect::<Option<Vec<_>>>()?;
let sketches = state_sketches.get(s)?.iter().map(|i| new(&new_sketch, *i)).collect::<Option<Vec<_>>>()?;
let mut st = st.clone();
let o = st.as_object_mut()?;
o.insert("bodies".into(), json!(runs(&bodies)));
o.insert("sketches".into(), json!(runs(&sketches)));
out_states.push(st);
}
let mut out_bodies = Vec::new();
for (m, r) in model_bodies.iter().enumerate().filter(|(m, _)| kept_bodies.get(*m) == Some(&true)) {
let (body, named) = (new(&new_kernel, *body_of.get(m)?)?, new(&new_names, at(r.get("names"), names.len())?)?);
let mut r = r.clone();
let o = r.as_object_mut()?;
o.insert("body".into(), json!(body));
o.insert("names".into(), json!(named));
out_bodies.push(r);
}
let mut out_features = Vec::with_capacity(features.len());
for f in features {
let state = new(&new_state, at(f.get("state"), states.len())?);
let mut f = f.clone();
let o = f.as_object_mut()?;
match state {
Some(s) => o.insert("state".into(), json!(s)),
None => o.remove("state"),
};
out_features.push(f);
}
let records = json!({
"features": out_features, "states": out_states, "model_bodies": out_bodies, "names": kept_of(names.as_slice(), &names_used),
"sketches": kept_of(sketches.as_slice(), &sketches_used),
});
let kept: Vec<usize> = (0..self.bodies.len()).filter(|i| kernel_used.get(*i) == Some(&true)).collect();
let bodies = kept.iter().filter_map(|i| self.bodies.get(*i).cloned()).collect();
Some((SavedModel { records, bodies }, kept, used))
}
}
impl Model {
/// The model a design file kept for `doc` ([`Model::saved`]), taken as `doc`'s evaluation:
/// every feature's result as saved, nothing evaluated. Refused (an error) unless every
/// feature's fingerprint is the one `doc` gives it now, and every record is well formed.
/// every feature's result as saved, nothing evaluated but the features whose state the file
/// did not keep ([`SavedModel::thinned`]). Refused (an error) unless every feature's
/// fingerprint is the one `doc` gives it now, and every record is well formed.
pub fn adopt(doc: &Document, saved: &SavedModel) -> Result<Model> {
let states = saved.states()?;
let records = &saved.records;
@@ -375,16 +533,24 @@ impl Model {
let step_digests = StepDigests::of(doc, &StepDigests::default());
let marker = doc.marker.unwrap_or(usize::MAX);
let ctx = context_digest(doc, &vals);
let mut cache = EvalCache::default();
let mut before = Arc::new(ModelState::default());
// Whether `before` is the state before the feature: one the file left out only stands in.
let mut exact = true;
let mut prev = 0;
let mut results = Vec::with_capacity(features.len());
for (i, (f, r)) in doc.features.iter().zip(features).enumerate() {
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests);
if r.get("id").and_then(Json::as_u64) != Some(f.id) || unhex(r.get("fingerprint").unwrap_or(&Json::Null))? != prev {
let saved = unhex(r.get("fingerprint").unwrap_or(&Json::Null))?;
// A fingerprint reads the state before its feature (where the bodies it takes lie).
// After a state the file left out, the saved one is taken: `evaluate` works that state
// out, checks the fingerprints after it, and evaluates again from one that differs.
prev = if exact { chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests, Some(&before)) } else { saved };
if r.get("id").and_then(Json::as_u64) != Some(f.id) || saved != prev {
return Err(bad("saved model: the document changed"));
}
let ms = r.get("ms").and_then(Json::as_f64).filter(|ms| ms.is_finite()).ok_or_else(|| bad("saved model: time"))?;
// A file without room for every state keeps some features without one (#3220).
let state =
r.get("state").map(|s| states.get(index(Some(s), states.len())?).cloned().ok_or_else(|| bad("saved model: state"))).transpose()?;
let result = FeatureResult {
id: f.id,
name: f.name.clone(),
@@ -398,27 +564,48 @@ impl Model {
skipped: r.get("skipped").and_then(Json::as_bool).ok_or_else(|| bad("saved model: skipped"))?,
stale: false,
fingerprint: prev,
state: states.get(index(r.get("state"), states.len())?).cloned().ok_or_else(|| bad("saved model: state"))?,
unkept: state.is_none(),
state: state.unwrap_or_else(|| before.clone()),
};
// Each result where an evaluation would have put it, so an edit undone (or set back)
// finds it again rather than evaluating the feature anew.
if !result.skipped {
let local = fingerprint(0, f, &vals, false, &step_digests) ^ placement_digest(doc, f);
cache.put(
cache_key(f, prev, local, ctx, &before),
CachedEval {
error: None,
warning: result.warning.clone(),
boolean_diagnostics: result.boolean_diagnostics.clone(),
ms,
bytes: result.state.retained_bytes(),
state: result.state.clone(),
},
);
exact = !result.unkept;
before = result.state.clone();
}
results.push(result);
}
Ok(Model { results, param_errors, step_digests, cache: Arc::new(std::sync::Mutex::new(cache)), ..Model::default() })
let mut model = Model { results, param_errors, step_digests, ..Model::default() };
let unkept = model.results.iter().filter(|r| r.unkept).count();
if unkept > 0 {
// The features whose state the file did not keep, each evaluated again from the state
// before it: nothing else in the model is missing then.
model.evaluate(doc);
log::info!(
"the saved model kept the states of {} of {} features: {} evaluated again",
model.results.len().saturating_sub(unkept),
model.results.len(),
model.last_recomputed
);
}
// Each result where an evaluation would have put it, so an edit undone (or set back)
// finds it again rather than evaluating the feature anew.
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));
cache.put(
cache_key(f, r.fingerprint, local, ctx, &before),
CachedEval {
error: None,
warning: r.warning.clone(),
boolean_diagnostics: r.boolean_diagnostics.clone(),
ms: r.ms,
bytes: r.state.retained_bytes(),
state: r.state.clone(),
},
);
before = r.state.clone();
}
}
Ok(model)
}
}
+23
View File
@@ -5,6 +5,29 @@
use serde::{Deserialize, Serialize};
use solidcraft_geom::Vec3;
/// The motion retained by an associative trace. Joint bounds keep their expressions, in
/// command units (degrees or millimetres); studies are referenced by their stable identity.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TraceMotion {
Joint { joint: u64, index: usize, from: serde_json::Value, to: serde_json::Value },
Study { study: u64 },
}
/// A generated root sketch curve and the inputs needed to regenerate it. The seed values
/// select the original assembly branch: playing or dragging the mechanism does not redefine
/// its trace. Older saved curves have no entry here and remain ordinary fixed sketch curves.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MotionTrace {
pub sketch: u64,
pub wire: String,
pub occurrence: Vec<u64>,
pub point: crate::joints::JointOrigin,
pub motion: TraceMotion,
pub samples: Option<usize>,
pub seeds: Vec<(u64, Vec<f64>)>,
}
/// Occurrences whose bodies may touch but not pass through each other while joints move.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContactSet {
+2 -7
View File
@@ -266,10 +266,7 @@ pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
// Every body's names are read below: work out the ones not known yet on all cores first
// (each body's names are its own, whichever thread works them out).
if legacy_name(name).is_none() {
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
let _ = naming::names(b);
});
naming::warm_names(st.bodies.iter().filter(|b| !b.body.is_mesh()));
}
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p)
}
@@ -445,9 +442,7 @@ pub fn locate_one(st: &ModelState, body: Option<&str>, name: &str) -> Result<Fac
st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh() && body.is_none_or(|n| b.name == n)).collect();
// As [`locate_face`]: the names of several bodies are worked out on all cores first.
if bodies.len() > 1 && legacy_name(name).is_none() {
solidcraft_kernel::for_each_on_threads(&bodies, 16, |(_, b)| {
let _ = naming::names(b);
});
naming::warm_names(bodies.iter().map(|(_, b)| *b));
}
let hit = locate_in(bodies.into_iter(), name, Vec3::ZERO).ok_or_else(|| format!("no face is named `{name}` (`model.faces` lists the names)"))?;
if hit.split {
+99 -1
View File
@@ -229,6 +229,23 @@ pub fn face_name_exact(b: &ModelBody, index: usize) -> bool {
names(b).verified(index)
}
/// Prepare only names not yet known before a lookup reads several bodies. A ready cell is
/// shared by snapshots and poses: dispatching threads just to read it costs more than a lookup.
pub(crate) fn warm_names<'a>(bodies: impl Iterator<Item = &'a ModelBody>) {
let bodies: Vec<_> = bodies.filter(|b| b.names.names.get().is_none()).collect();
#[cfg(test)]
tests::record_warm_jobs(bodies.len());
match bodies.as_slice() {
[] => {}
[body] => {
let _ = names(body);
}
_ => solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
let _ = names(b);
}),
}
}
/// A body's names, with what finding them again needs: from the history of the feature that
/// made it where it reports one, else from geometry (unverified).
pub(crate) fn names(b: &ModelBody) -> Arc<Names> {
@@ -583,7 +600,9 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
}
FeatureKind::Revolve { sketch, axis, .. } => {
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis, &|name| crate::eval::construction_axis_in(None, before, name, 0)) else {
return format!("F{fid}:face:{fi}");
};
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6) && ns.iter().all(|m| m.dot(d).abs() < 1e-6);
@@ -870,6 +889,85 @@ pub fn face_on_body(body: &ModelBody, name: &str, p: Vec3) -> Option<(usize, Vec
#[cfg(test)]
mod tests {
thread_local! {
static WARM_JOBS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(super) fn record_warm_jobs(n: usize) {
WARM_JOBS.with(|jobs| jobs.set(jobs.get() + n));
}
fn take_warm_jobs() -> usize {
WARM_JOBS.with(|jobs| jobs.replace(0))
}
fn lookup_bodies() -> ModelState {
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 2., 1.)).unwrap();
let mut st = ModelState::default();
st.bodies = (0..13).map(|i| ModelBody::new(format!("Part{i}"), body.clone(), i + 1)).collect();
st
}
fn same_hit(a: &FaceHit, b: &FaceHit) {
assert_eq!((a.body, a.face, a.point), (b.body, b.face, b.point));
assert_eq!((a.split, a.merged, a.verified, a.on_point, a.legacy), (b.split, b.merged, b.verified, b.on_point, b.legacy));
}
#[test]
fn cached_face_lookups_schedule_no_name_warmup() {
let st = lookup_bodies();
for b in &st.bodies {
let _ = face_names(b);
}
let name = face_names(&st.bodies[0])[0].clone();
let picked = locate_face(&st, &name, Vec3::ZERO).unwrap();
let unique = locate_one(&st, None, &name).unwrap();
take_warm_jobs();
// The two public lookup paths used by picked origins and named-face commands.
for _ in 0..145 {
same_hit(&locate_face(&st, &name, Vec3::ZERO).unwrap(), &picked);
same_hit(&locate_one(&st, None, &name).unwrap(), &unique);
}
assert!(locate_face(&st, "absent:face:0", Vec3::ZERO).is_none());
assert!(locate_one(&st, None, "absent:face:0").is_err());
assert_eq!(take_warm_jobs(), 0, "cached names must not dispatch preparation work");
}
#[test]
fn face_lookup_warms_only_cold_and_invalidated_names() {
// Exercise both entry points as the first reader of a partly named model.
for picked in [false, true] {
let mut st = lookup_bodies();
let name = face_names(&st.bodies[0])[0].clone();
let lookup = |st: &ModelState, name: &str| {
if picked { locate_face(st, name, Vec3::ZERO).unwrap() } else { locate_one(st, None, name).unwrap() }
};
take_warm_jobs();
let hit = lookup(&st, &name);
assert_eq!(take_warm_jobs(), 12, "only the other bodies need names");
let snapshot = st.clone();
same_hit(&lookup(&snapshot, &name), &hit);
assert_eq!(take_warm_jobs(), 0, "snapshots share ready names");
st.bodies[0].name = "Renamed".into();
st.bodies[0].names = st.bodies[0].names.renamed();
let renamed = lookup(&st, "Renamed:face:0");
same_hit(&renamed, &hit);
assert_eq!(take_warm_jobs(), 1, "a rename invalidates only its own names");
assert!(locate_face(&st, &name, Vec3::ZERO).is_none());
same_hit(&lookup(&snapshot, &name), &hit);
assert_eq!(take_warm_jobs(), 0, "the retained snapshot stays ready");
let source = st.bodies[1].clone();
st.bodies[1].names = NameCell::same(&source);
same_hit(&lookup(&st, "Renamed:face:0"), &renamed);
assert_eq!(take_warm_jobs(), 1, "a lazy moved copy is prepared once");
assert!(Arc::ptr_eq(&names(&st.bodies[1]), &names(&source)));
same_hit(&lookup(&st, "Renamed:face:0"), &renamed);
assert_eq!(take_warm_jobs(), 0);
}
}
/// Pieces lying almost level along the sort axis must keep the order they arrived in, whatever
/// the low bits of their centres do. The centre is the mean of a tessellated boundary, so those
/// bits depend on where the loop's polyline starts and can differ between processes — which is

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