Compare commits

...

239 Commits

Author SHA1 Message Date
bherbruck 5965439a15 Release 0.24.0 2026-10-26 19:23:21 -04:00
bherbruck 0b2d3ecd44 Release 0.24.0: version bump, design format 39 (#7394)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:23:19 -04:00
github-actions[bot] 96e43bb4ef Regenerate generated files and fold pending files [skip ci] 2026-10-26 23:04:43 +00:00
github-actions[bot] 955f5aea43 Regenerate generated files and fold pending files [skip ci] 2026-10-26 22:56:34 +00:00
bherbruck 739e442824 Links: ask before following a design's network paths (#7372)
A path a design names (a linked design, a sketch text's font file) now
goes through one rule before any file call: a network location is
reached only once its server is allowed, a device path never. Update
Links and Open Linked Design ask first (Allow once / Always allow this
server / Cancel); Preferences > Design lists the allowed servers and
can allow every network location. component.links reports such a
link's found as unknown with a reason, without looking for it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:52:43 -04:00
MAKAO 30f951ecf9 Navigation bridge: a device's own app moves the view (#6094)
A homemade 3D input device's driver app (MAKMOUSE) listens on a loopback
port; with Preferences > Navigation > "Navigate with a device app on this
computer" on (off by default), the desktop app connects, says hello and
reads JSON lines: axis motion (orbit, pan or zoom by mode), zoom steps and
standard views. The motion goes through navigation::device like a 3D
mouse's, only in the focused window; a HID device the app names is left
to it. Documented in docs/3d-mouse.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 22:14:52 +00:00
github-actions[bot] 02458892d9 Regenerate generated files and fold pending files [skip ci] 2026-10-26 22:30:02 +00:00
amolinari-autotex 1d02a5f2d4 Personal Hole parameter presets (#4480)
* feat: add personal Hole parameter presets

* feat(hole): prompt for preset names and recall changed selections

Ask for a name only on Save new or Rename in the kit confirmation. Keep Enter and Escape inside the prompt, preserve cancelled drafts, recall only changed selections, and retain independent recessed-hole bottom choices.

* feat(hole): preserve thread direction and partial depth in presets

Carry Left/Partial, explicit source units and Custom draft guards through personal preset capture and recall. Preserve legacy Right/Full/flat defaults and reject unknown options before changing the form. This production bridge depends on the handedness and graphical-depth changes inherited below it.

* docs(hole): describe supported preset thread options

* feat(ui): save modified hole presets in place

* fix(presets): retain reference placement and current locale contracts

* test(ui): borrow restored preset library during serialization

* test(presets): preserve modal focus with viewport focus restoration

* Use UI kit controls for hole preset selection
2026-10-26 22:00:56 +00:00
bherbruck c391ef17cb Themes: Clay (light) and Clay Dark, warm paper and charcoal (#7342)
* Themes: Clay (light) and Clay Dark, warm paper and charcoal with a terracotta accent

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

* Clay: on-accent text a touch lighter for 4.5:1 on the accent

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:31:40 +00:00
Nathan Dane e413327821 History: follow the Browser's translucency when docked under it (#6922) 2026-10-26 21:26:28 +00:00
HiroKenEz a1eb63c169 Extrude: the picked region is the one extruded again (#6724) (#6766)
* claim: #6724

* Extrude: the picked region is the one extruded again (#6724)

Since #5530 (#5302) every index or point pick grew to the whole drawn
profile that reference geometry cuts, so a lens or crescent clicked in
Extrude, where the face's projected outline cuts a circle, extruded the
whole circle while the dialog highlighted and counted one region.

Dialogs now send exact_regions: true with their profile picks, and the
command keeps each pick to the region it names. Picks without the flag
(scripts, agents, the CLI) still take the whole drawn profile (#5302).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:02:43 +00:00
github-actions[bot] 610f2e479c Regenerate generated files and fold pending files [skip ci] 2026-10-26 21:16:46 +00:00
HiroKenEz f49cd33db1 Select Other: list what is under the pointer front to back, one body at a time (#5345) (#5509)
* claim: #5345

* Select Other: list what is under the pointer front to back, one body at a time (#5345)

The list was sorted by kind first, so on stacked bodies it showed every
body's face, then every body, mixed across depth. Now each body's rows
stay together (vertices, edges, face, then the body), bodies ordered by
their nearest hit along the pick ray; an edge counts at its point under
the pointer. Sketch overlays stay first, profiles, planes and axes last.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:55:45 +00:00
Colin Faulkingham 4c7087ec16 Disable Perspective while editing a sketch (#2144) (#4291)
* claim: #2144

* Disable perspective toggle while editing a sketch (#2144)
2026-10-26 20:48:21 +00:00
AhmetV f87b657c82 Sketch: keep the dimension box focused after a palette linetype click (#2927) (#3197)
Co-authored-by: AhmetV <344941595+ahmetv10@users.noreply.github.com>
2026-10-26 20:32:10 +00:00
CamWight 64ba396c05 Part properties: one table of keys (#4784) (#4786)
* claim: #4784

* Part properties: one table of keys, each with its scope, check and label (#4784)

SolidCraft's part-property keys (project, part number, revision, author,
organization, description, licence) were declared once but listed again
by hand in each command, error, dialog and window. They are now one
table, metadata::KEYS: each row has the key, where it may be set
(Scope: the design, a component or both), how its value is checked and
the message id of its label. A new key is one row.

Metadata::set is told whose metadata it changes (Owner::Design or
Owner::Component), and a key is taken where its scope covers that
owner. is_well_known, both set commands, document.metadata,
component.metadata, the refusals, the Design Properties dialog and the
Bill of Materials window read the table; the hand-written lists are
gone (WELL_KNOWN, the label match whose fallback named any unknown key
"License", the window's three labels). metadata::BOM_KEYS names, in
column order, the keys a bill of materials lists for each part:
component.metadata returns them, component.metadata.set takes each as a
parameter of its own and the window edits them; a test keeps each one a
row of the table that a component takes. Free properties use the keys'
text rule (metadata::check_text) instead of a copy of it. The params
docs point to document.metadata's properties instead of repeating the
keys.

Every key accepted before still is, on the design and on a component.
Two things change:
- a refused key or a full metadata on a component says "part property"
  ("`bogus` is not a part property: …", "part properties are limited
  to …"), not "design property";
- changing the design's own properties no longer evaluates the model.
  The fields that change no geometry are named once,
  Document::with_part_properties_of: the design's metadata, and each
  component's metadata and free properties. It compares them first and
  copies the design only when one differs, as before.

The shortcut is for a session's own edits only. A command that gives
the session another design (doc.open, file.new, doc.recover: through
Session::replace_design, which now counts them) is refreshed in full,
however little the new design differs from the one before, so its
joints are solved and its occurrence state pruned. Before, opening a
design that differed from the open one only in render settings or
component properties skipped that; with the design's properties in the
list it would have for two revisions of one part too.

Tests: the table (one row per key, the dialog's order); the bill of
materials' keys (rows a component takes, in column order); each scope
on each owner, and refusals that name the owner (the empty key, an
unknown key, a 256-byte key shown cut); the text check at 4096 bytes,
line breaks, tabs and control characters; hostile keys and values
refused without a change; only-part-property changes in each scope,
and changes beside them (units, a component's name, an added, removed
or swapped component); document.metadata.set, remove, undo and redo
evaluate nothing; opening a revision that differs only in its author
solves its joint; the component wording, the shortcuts and the limits;
the dialog's and the window's labels from the table.

What the tests can't reach: every row is Scope::Both, so a known key
refused for its owner (the wrong-owner branch of check) can't happen
yet. It becomes live with the first single-scope row, bom_type, a
component-only key.

Left for later (not this topic):
- Metadata::others() ignores scope, so a component-only key stored on
  the design would be neither a dialog field nor listed under other
  tools' data. It matters from the first single-scope key (bom_type).
- assembly.bom's rows, its CSV columns and the window's grid headers
  still name the three keys by hand; the bill of materials module's
  rewrite will read BOM_KEYS.
- The bill of materials types (P1, #5107, into drawings) still use
  WELL_KNOWN and the two-argument set: once this is on main, bom_type
  is rebuilt on it as one KEYS row with Scope::Component, set through
  Owner::Component.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:03:22 +00:00
Logan Night 1720c56840 Drag arrows: pattern spacing and sketch offset go negative past their start (#5497) (#5508)
* claim: #5497

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

* Sketch pattern: a spacing arrow dragged past zero gives a negative spacing (#5497)

The drag arrow floored an unsigned value at a small positive number, and the
rectangular sketch pattern's two spacing arrows were unsigned, although the
command takes a negative spacing and typing one works. Both arrows are signed
now, like the Extrude, Move and offset ones.

The test drags each arrow back past its start: without the change the first
one stops at 0.001.

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

* Drag arrows: the solid pattern spacing and one-sided sketch offset go negative too (#5497)

The solid rectangular pattern's Spacing arrow and the sketch Offset distance
arrow were unsigned, so a drag back past the start stopped at a small positive
value, although both commands take a negative value and typing one works. Both
are signed now. A symmetric offset uses only the distance's size, so its arrow
still stops at the curve.

Tests: the solid pattern arrow dragged 10 mm back gives Spacing -10 and the
copies on the negative side (without the change: 0.001); the offset arrow
dragged across its circle gives -1 mm and the same circle as typing -1 mm
(without the change: 0.001), and a symmetric offset stays positive.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 20:03:03 +00:00
github-actions[bot] b28decfaf6 Regenerate generated files and fold pending files [skip ci] 2026-10-26 20:30:44 +00:00
bherbruck 9e4dc48165 Release: sign Windows binaries and MSI through SignPath Foundation (#7067)
* Release: sign the Windows binaries and MSI through SignPath Foundation

When the SIGNPATH_API_TOKEN secret exists, each Windows job sends the
three .exe files to SignPath before packaging, then the MSI built from
them, using the GitHub artifact flow from SignPath's docs. The policy
is test-signing unless the signpath_signing_policy dispatch input or
the SIGNPATH_SIGNING_POLICY repository variable says otherwise. Without
the secret the jobs build unsigned exactly as before.

package.ps1 gains -Phase stage|package so signing can sit between the
build and the MSI; the artifact configuration to paste into SignPath is
packaging/windows/signpath-artifact-configuration.xml.

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

* SignPath: drop the description attributes the Foundation plan doesn't allow

SignPath Foundation projects get an automatic signature description (its watermark);
the artifact configuration may not override it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:47:06 +00:00
bherbruck 3de2f96b7f Sketch dimensions: double-click a shown dimension in the model view to edit it (#7079) (#7080)
* Double-click a shown sketch dimension in the model view to edit its value (#7079)

A finished sketch's dimensions shown in the model view (Show Dimension) now take a
double-click: the same inline value box as sketch editing opens at the text, Enter
changes the dimension's own expression through parameters.change (one undo step), Esc
cancels. The sketch is not edited and the view does not move. Driven dimensions stay
read-only with a status hint.

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

* Control channel: ui.selection lists the shown dimensions of finished sketches

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

* Test: a shown dimension that breaks a later fillet reports it, and Undo restores both

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:47:04 +00:00
Jesper We 436ee95095 Preference: return to the previous view when a sketch ends (#7038) (#7043)
* claim: #7038

* Preference: where the view goes when a sketch ends (#7038)
2026-10-26 19:31:55 +00:00
github-actions[bot] 1ee2532678 Regenerate generated files and fold pending files [skip ci] 2026-10-26 20:00:04 +00:00
github-actions[bot] 3152e02fe5 Regenerate generated files and fold pending files [skip ci] 2026-10-26 19:51:41 +00:00
Nathan Dane fbcd4852c1 Navigation bar: the view commands (#6585) (#6586)
* claim: #6585

* Navigation bar: the view commands (#6585)
2026-10-26 17:46:15 +00:00
HiroKenEz 97cce94a1c Agent chat: tool calls read as summaries, raw JSON behind Details (#1457) (#6245)
* claim: #1457

* Agent chat: tool calls read as summaries, raw JSON behind Details (#1457)

Each tool-call card now says what the call did, built from the command's
translated label, its main parameters (lengths in the design's units) and the
bodies the result names: "Extrude Sketch1 by 10 mm", "Box 40 x 30 x 20 mm,
then Fillet 4 edges (r 4 mm)", "Measured Block: ...", "Saved block.solidcraft".
Calls it can't put in words keep the agent's title. The raw arguments (now
kept from the in-app client and ACP's rawInput) and the result sit under a
folded "Details".

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

* Agent chat cards: show file names, never paths; temp files not named

ui.screenshot reads "Screenshot"; save/export/open and other tools' titles
show only the file name. The full text stays under Details and in the tooltip.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-26 17:22:17 +00:00
Victor Santos Oliveira 5b41d61f1e Tests: exercise home-path tooltips on Windows (#6890) (#6891)
* claim: #6890 portable home-path tooltip test

* test(ui): exercise native home paths on Windows (#6890)
2026-10-26 17:04:43 +00:00
Dahak Builds 2863b80574 Sketch: 3D arc command (#5681) (#5682)
* claim: #5681

* sketch: exact circular arcs on drawn 3D wires (#5681)

Add WireArc (centre, unit normal, radius, start/end) as an optional analytic
form on drawn 3D sketch wires, with arc_from_3_points, arc_sweep and
sagitta-bounded arc_polyline; add_drawn_arc_in stores the arc, its three
defining fit points and its samples; move_drawn_point re-derives the arc
atomically and refuses collinear points; validate_wires checks saved arcs.

* engine,ui: sketch.arc3d exact 3D arc command (#5681)

sketch.arc3d draws an exact circular arc through three world points in a 3D
sketch, stored analytically beside its fit points and samples; sketch.move3d
rebuilds it, sketch.inspect reports centre/radius/sweep; placement mapping
(map_wire, now shared with the engine's world state) transforms the arc.
One pending design-format file records the new field. The viewport's 3-Point
Arc tool draws a 3D arc when clicked off the sketch plane, with a live arc
preview; the SKETCH panel offers sketch.arc3d.

* ui,engine: 3D Arc button starts an interactive tool (#5681)

sketch.arc3d now maps to a Draw(3) tool handled by sketch3d: three world clicks (start, end, through - same order as the 2D arc tool) run the command, with the live arc preview and snapping. The command is only enabled while a 3D sketch is open, so the toolbar never offers a tool that could only fail. Locale labels for the command.

* ui: cargo fmt

---------

Co-authored-by: RayGalt <RayGalt@pm.me>
2026-10-26 16:29:39 +00:00
yhamil04 03a701d678 Viewport tests: the Metal hover pixel test keeps its pictures when it fails (Refs #1383) (#6805)
* claim: #1383

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

* Viewport tests: the hover pixel test keeps its pictures when it fails (#1383)

The Metal hover pixel test failed once in a loaded full-suite run and has
not been reproduced since. On failure it now saves the plain and hovered
GPU and CPU frames and a mask of the pixels that do not light, and names
the folder in its message. The assertion is unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:01:39 +00:00
github-actions[bot] a2ccbd0684 Regenerate generated files and fold pending files [skip ci] 2026-10-26 19:21:19 +00:00
Daniel 2e566e2c58 Placement handles for Rest, Boss and Snap Fit (#4384) (#5362)
* claim: #4384

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

* Placement handles: shared module, with Rest as the first tool

The Rest handles spike becomes a shared module, place_gizmo, so other features placed on a face can
use the same marker and knob. The mechanics (crosshair with its three states, the grab offset, the
footprint while dragging, the turn knob) live in place_gizmo.rs. The handles own the pointer through
canvas::owns_pointer, as the move triad does, so navigation and Select Other's press-and-hold leave
them alone. Each tool is one file under place_gizmo/ that gives a Spec: which dialogs it serves,
which commands' Position input also takes sketch points, where the place is, how the pointer
re-places it, its footprint and its turn, if any. The module lists the tools one per line, so tools
added on separate branches each add one `mod` line and one list line. Rest is the first.

The handles are always on; the preference the Rest spike had is gone. The POINTS accept flag on a
Position input is granted from the tools' Specs instead of a Rest-only arm in the plastic dialog's
start. Handles are published as `rest:position` and `rest:rotate` as before.

Seen straight down, the dialog's height arrow collapses onto the place and its value box sits just
right of it, where it took the press meant for a knob pointing that way. The knob now slides out
along its ray until it is clear of the box.

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

* Placement handles: Boss

The marker moves a boss across the model, also from Edit Feature, and its Position input takes
sketch points while the handles are on, so a screw boss can be dragged onto the centre of a
circle drawn for it. A boss is round, so it gets no knob; the footprint while dragging is its
outer circle. Published as `boss:position`.

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

* Placement handles: Snap Fit

The marker moves a snap fit across the model and its Position input takes sketch points while
the handles are on, so the arm can be put where a sketch says (#3423). The knob points the
catch: it turns between the four directions the Catch Direction combo offers for the face, so
the knob and the combo always agree; the command keeps only those four, so there is no free
turn. The footprint while dragging is the arm's foot, its thickness behind the place and its
width across. Published as `snap_fit:position` and `snap_fit:rotate`.

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

* Placement handles: the look in one file

Everything that draws the handles (the crosshair and its three eased states, the knob's ball, the
cased lines, the angle label, the colours) moves from place_gizmo.rs into place_gizmo/look.rs, so a
restyle touches one file and the mechanics (hit radius, drags, knob geometry) stay where they are.
No change in behaviour.

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

* Placement handles: behind a Design preference, off by default

The handles sit behind Preferences > Design > Placement > Placement handles (`placement_handles`,
also through `ui.prefs`), off by default as the hole placement work in #2912 is: a Rest, Boss or
Snap Fit is then placed by its click alone, its Position input takes faces only, and no handle is
drawn or published. The tests switch the preference on; one checks the default.

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

* Placement handles: the tests hide the origin through set_origin_shown, as main does since #1849

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

* Placement handles: the move marker as designed in the prototype, drawn with theme tokens

The marker is a dot with four arms to the screen cardinals: short and dotted at rest, longer with open arrowheads when pointed at, solid while dragging, each state growing out of the previous one (360 ms). Sizes are the prototype's. Its core is the icon highlight and its casing the marker halo, the tokens main's hole handle uses, so the literal allowance goes. The tip shows while pointing, not during a drag.

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

* Placement handles: the dialog's drag arrow stands above the move marker

With the handles on, the Height arrow started at the marker's centre and ran through its upper arm, head on arm tip. The arrow now keeps the marker's full reach and a gap free at the base, its shortest length grows by as much, and the base dot is left to the marker. Other dialogs are unchanged (clearance zero).

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

* Placement handles: a turn ring on the face replaces the knob

The ring lies on the face about the move marker's centre, drawn in perspective, a fixed 65 px on screen like the move triad's arrows (it is a control of something on the screen, not a measure of the model). At rest it is a short dotted arc with an open chevron grip just outside; pointed at, the dotted circle opens from the grip both ways round and the grip slides in and fills; while turning, a solid arc around the grip. Dragging the grip turns the feature as the knob did, through the same handle name, so the harness tests stand.

The floating value box yields: with a handle's centre under it, it moves to the other side of the arrow's tip or above it, since seen straight down the box sat exactly where the grip is. A handle the arrow's stem crosses keeps the pointer.

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

* Placement handles: a compass while turning, and the turn pulled onto the face's compass points

Four ticks just outside the ring at the face's in-face world directions (the ones the Snap Fit combo names, +X, −Y, …) appear while turning and stay put, so you can see how far you have gone. A Rest's turn is pulled onto one of them within 8°, otherwise it steps by 5° as before; Alt or Ctrl frees it. The readout beside the grip names the point it sits on ("90° · +X") and keeps clear of the ring.

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

* Placement handles: the footprint in the preview's blue, the idle handle steps back

The footprint drawn while the marker drags is a filled shape in the preview's own blue with a thin darker edge, not a white outline, so white stays reserved for what can be grabbed. While one handle is dragged the other fades to a quarter and comes back when the drag ends.

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

* Placement handles: the ring in the preference text, the docs and the tests' wording

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

* Placement handles: the look as tuned in the app

Marker: drawn to the redrawn design (dot r 3.9 pt, two dashes per arm, a filled head 8.7 × 7.1 pt whose base is where the outer dash began); pointed at, the outer dash becomes the head (its far end narrows to a point, its near end widens); dragging runs the inner dash on into the head as a shaft. Every casing is 0.4 pt outside the edge, for lines, heads and the dot alike.

Ring: the grip and the compass lie in the ring's plane like the circle, so the grip is never skewed against it; the grip is a thick chevron with a hole that flattens into the base as the grip slides onto the ring, and fills; the dotted circle stays while turning, with the solid arc laid over it; opening eases in and takes 0.3 s, leaving is a 0.15 s fade over the resting ring, not the sweep backwards; a drag keeps the angular gap of the press, so the grip does not jump onto the pointer; the compass ticks and the readout's point name are in their axis colours, and the grip takes the axis colour (eased) while aligned with one, with that point's tick left out; free turning keeps a hair of pull (0.5°) so an axis can still be hit; the readout wraps to a half turn.

Value box: with a ring it keeps out of the circle the ring, grip, compass and readout sweep, one place per view, nothing moves when a turn starts; also when the value does not evaluate and there is no arrow.

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

* Placement handles: the readout shows a half turn as 180°, not −180°

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

* Placement handles: the motions in one place

The animations the handles grew (a factor eased in and out each way on its own time and easing, a handle stepping back while the other works, a colour that comes and goes from the last one it was on, the gap a press keeps for its drag, an offset that slides instead of jumping) move out of place_gizmo into motion.rs, so the next manipulator reuses them. The look's timings become Transitions in look.rs. No change in behaviour: the contact sheet before and after is identical, the harness tests stand.

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

* Placement handles: the axis colours as main names them since #4430

The compass and the aligned grip take the one set of X, Y and Z colours (axis_x, axis_y, axis_z); the Move gizmo's own array is gone.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-26 15:35:10 +00:00
amolinari-autotex 47285ccb88 Thread: right- and left-handed direction (#4414) (#4415)
* claim: #4414

* feat: persist right and left thread handedness (#4414)

* fix(thread): verify handedness with referenced holes and complete locales

* test: verify handedness with merged tapped thread catalogues

* test: retain tapped thread depth when changing handedness (#4414)

* test: restrict Unix symlink coverage to Unix targets

* test: inspect hole thread depth inside edit command parameters (#4414)

* test(doc): initialize cylinder radius in handedness fixture
2026-10-26 13:57:08 +00:00
github-actions[bot] da16362626 Regenerate generated files and fold pending files [skip ci] 2026-10-26 18:45:42 +00:00
amolinari-autotex 13cd45d1f1 Apply complete feature transactions off the window thread (#3133) (#3135)
* claim: #3133

* Apply complete feature transactions off the window thread

Evaluate dependent history and prepare reference and GPU display caches before accepting a guarded result. Preserve the evaluated original model for safe editor cancellation and journal only accepted commands.

* fix(ui): preserve original marker across sketch-to-feature edits

* test(ui): verify edit-sketch cleanup and guard Unix link fixture

* test(ui): fix texture disposal and unreachable assertion in feature tests

* fix(build): compile CLI link inspection only on Unix
2026-10-26 13:27:04 +00:00
Nathan Dane c5e63b7871 macOS DMG: blueprint window with larger icons and a drag arrow (#5831) 2026-10-26 12:50:24 +00:00
Steven Waters 2755c9ccfb Pin auto-generated body names (#6402) (#6403)
* Pin auto-generated body names

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

* Fixed body counter

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

* Fixed formatting

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

* Altered test behaviour to reflect new pinned body names

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 12:33:58 +00:00
SS-EN 4ab6d92952 Agent chip: the batch step and its command, centred above the navigation bar (#5748)
While an agent's batch computes off the window thread (#1989), the chip reads
N/M · <command>. It is centred on the navigation bar, just above it, and moves up
past any toast or panel it would cover.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:28:30 +00:00
froglemonade2 e063a7778e Axes and points from circular edges and round faces (#3128) (#4610)
* Axes and points from circular edges and round faces

In 3D, a round edge or face stands for its circle wherever a command
takes an axis or a point.

Revolve, Surface Revolve, both Circular Pattern dialogs, Plane at Angle
and Move -> Rotate take a circular edge (through its centre, along its
normal) or a cylinder, cone or torus face (its axis). The features keep
the pick as a model reference, so they follow the model when a parameter
moves it. Straight edges work as before; flat faces are refused.

Every 3D point input takes a circle's centre: construction points (which
follow the circle), To Object, patch points, hole placement, and Move's
pivot and points. Hovering shows the axis as a dashed construction line
or the centre as a star, and the dialog's chip names what the pick
stands for ("centre of edge 10", "axis of face 3").

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

* Format the round-pick hover check

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

* Selection chips: name a round pick as a circle centre or axis

The chip has room for little more than the body and a word or two:
"Shaft · circle centre", "Shaft · circle axis", "Shaft · face 2 axis",
and "Shaft · point" for a point of the model.

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

* Move: a circle's centre in a placed component is where the component shows it

Move's pivot and points read a picked circle from the model in its
component's own frame, while the pick is in the world. They now read it
from the world state (and, editing a Move, from the state before it,
placed the same way), as the viewport and the other round picks do.

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

* Plane at Angle on a planar face about a model edge: test that it follows both

Upstream #1956 made a planar face a base for Plane at Angle; this branch makes a model edge an
axis. The rebase keeps both in one PlaneRef::AtAngle.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:27:32 +00:00
Dahak Builds 8630a3f0e5 External STEP corpus: nightly import checks (#1397) (#6407)
* claim: #1397

* External STEP corpus: nightly import checks on real-world files (#1397)

tests/external-corpus.toml pins 20 real-world NIST PMI STEP files (AP203
geometry-only and AP242 editions from several vendor exporters, public
domain) by archive and per-file SHA-256 with the licence recorded.

cargo xtask external-corpus downloads each source once into the gitignored
corpus/external/, verifies every hash, imports each file headless
(file.insert_step + inspect.measure) and checks body count, total volume
(0.1 %) and area (0.5 %) within tolerances and time and memory budgets.
A file whose upstream hash changed is reported as 'source changed', never
as our failure. --bless "why" re-records expectations in the manifest with
a dated reason. The External corpus workflow runs it nightly.

assets: skip corpus/ when scanning for attribution (downloaded case data
is never committed).

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 12:26:36 +00:00
dependabot[bot] 0e0c23b080 Bump pillow from 12.3.0 to 13.0.0 in /assets/render/starter/tools (#6409)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.3.0 to 13.0.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.3.0...13.0.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 13.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 12:26:15 +00:00
semmelrinde e29075376c Curvature Map: a scale that lightens steadily from flat to tight (#975) (#6136)
The blue, green, red scale's lightness rose and then fell (L* 44 up to
72 and back to 50), so a flat face and a tight radius looked alike with
hue lost, and its upper part merged for a red-green loss. The stops are
now dark blue, a middle grey-gold and light yellow in one straight
blend, as measured in the issue: L* rises the whole way and every
quarter step stays clear for normal, protan, deutan and tritan vision.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:25:11 +00:00
Karl Von Richter 44b0056d48 Frames: a Weldments ribbon tab (#4003) (#5262)
* Frames: a Weldments ribbon tab (#4003)

- WELDMENTS joins the ribbon after SHEET METAL and is built the same way:
  Sketch (New Sketch), Create (Frame Member) and Configure (Parameters,
  Configurations). Frame Member's home moves there from MODEL > Create.
- Always shown, as a frame starts from a sketch of its lines. It selects
  itself and takes the contextual style when the selected body (or the last
  body of the active component) is a frame member; sheet metal comes first.
- It uses the default accent until the palette has a colour for it.
- The tab label is in every catalog; docs/ribbon-layout.md, docs/frames.md
  and the tab list in docs/control-protocol.md follow.

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

* Frames: Weldments keeps the ordinary tab look (#4003)

Weldments still selects itself when a frame member is the active body, but
no longer takes the solid contextual fill or the 2 px accent line under the
tab row: selected, it looks like a selected Model tab. Only Sketch and Sheet
Metal (on a sheet body) are contextual.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:24:38 +00:00
ivisaf 3bcf77ee32 Expressions: a number and its unit are one value (10 mm / 2 mm is 5) (#6331)
* Expressions: a number and its unit are one value (10 mm / 2 mm is 5)

A unit written after a number was multiplied in at the precedence of `*` and
`/`, so `10 mm / 2 mm` was (10 mm / 2) mm = 5 mm² and `90 deg / 1 rad` a value
in angle², reported as a unit error. The number now takes the unit after it
before `*` and `/` apply; a fraction of numbers before a unit stays one value
(`1/2 in` is half an inch), and an exponent stays a plain number (`2^3 mm`).

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

* Expressions: after a division, a fraction before a unit only when closed up

`width / 10 / 2 mm` read the `10 / 2 mm` as the fraction 5 mm and gave
width / (5 mm) = 8 instead of (width / 10) / 2 mm = 2. After a `/` the
fraction rule now needs the fraction written without spaces (`1 in / 1/2 in`
is still 2); elsewhere `1/2 in` and `10 / 2 mm` are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:24:34 +00:00
Victor Santos Oliveira 91638cb1f3 Add practical command guides and a contribution template (#5237) (#6772)
* claim: #5237 command-level guide follow-up

* docs: add practical command guides and resource contributions (#5237)

* docs: index practical guides and reject incomplete command links
2026-10-26 12:24:23 +00:00
raykholo 66b872a749 Web app: open a design from a same-origin path (#6670)
?open= now also accepts an absolute path on the page's own origin, fetched
with the page's scheme, so a self-hosted setup that serves SolidCraft and
other tools under one origin can link a design on a LAN without a public
HTTPS address. Absolute URLs keep the HTTPS-only rule (a URL on the page's
own origin is also accepted), redirects must end on HTTPS or the same
origin, and protocol-relative, dot-segment, backslash and control-character
values are refused.
2026-10-26 12:23:52 +00:00
github-actions[bot] 2016ba71f4 Regenerate generated files and fold pending files [skip ci] 2026-10-26 18:07:03 +00:00
ChrisNo 7556e23fe7 Reuse existing tabs when reopening saved designs (#2413) (#2414)
* claim: #2413

* Reuse existing model tabs when reopening saved designs (#2413)

* Expect Open Recent to reuse the existing design tab (#2413)

* Update tab-reuse regression for session visibility
2026-10-26 12:04:11 +00:00
semmelrinde 4d06daf87b Patterns: one before the feature it copies follows that feature's parameters (#6726) (#6727)
* Patterns: one before the feature it copies follows that feature's parameters (#6726)

A pattern or mirror of features made with the timeline rolled back comes
before its sources and replays them from their definitions. Its
fingerprint chained only the features before it and its own inputs, so a
change of a source's parameter left its result in place: the box grew,
its copy kept the old length, also after rolling forward.

The chain fingerprint of a pattern or mirror of features now also takes
the fingerprint of each feature it replays (and of their sources, for a
pattern of a pattern), wherever they are in the timeline.

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

* Patterns: a test for a later feature taking a deleted source's name (#6726)

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

* Patterns: fingerprint the replayed sources with the document, as fingerprint now takes it (#6726)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:01:58 +00:00
rlartot c7e53899af Updated AppImage takes the release file name (#3538) (#3658)
* claim: #3538

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

* Updated AppImage takes the release's file name (#3538)

A versioned AppImage name (solidcraft-<version>-linux-<arch>.AppImage) is
replaced by the new release's name, so zsync-based tools keep finding the
update data. Other names are kept.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:58:47 +00:00
Nathan Dane 18ca30a311 fix(ui): open Appearance editor from context-menu parent (#5826) 2026-10-26 11:57:04 +00:00
Colin Faulkingham b55f30959d AI Preferences: reflect agent authentication results (#2578) (#4253)
* claim: #2578

* fix: reflect agent authentication in login badges (#2578)
2026-10-26 11:57:00 +00:00
semmelrinde 8b98f6c7e3 Sketch dimensions: a length's label says its unit (#1155) (#6131)
A sketch dimension read Ø40.00, with nothing to tell millimetres from
inches, while the value inputs and the pointer's coordinates show the
unit. Length labels now end with the design's unit (Ø40.00 mm, 1.000 in,
(25.00 mm) for a driven one); angles keep their degree sign.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:56:57 +00:00
bherbruck 3a504f5347 Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439) (#6444)
AGENTS.md drops from 16.8k to 3.7k characters: the hard rules, each naming the
check that enforces it, and a table of what to read for each kind of work.
The rest moves into eleven new skills; the five existing ones stay.

cargo xtask skills fails when AGENTS.md or a SKILL.md cites a path, an xtask
command or a skill that does not exist, when the router table and skills/
disagree, or when the router or a new skill grows past its size cap. It runs
in cargo xtask ci and the repository checks job.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:51:19 +00:00
Colin Faulkingham 7f2acbc004 Claim bot: only actual claims reserve issues (#6739) (#6775)
* claim: #6739

* fix(claim): reserve issues only for actual PR claims
2026-10-26 11:50:18 +00:00
Dahak Builds d3720041b6 CI tooling: opt-in bound for local test concurrency (#1188) (#6831)
* claim: #1188

* #1188: split per-binary test threads and matrix workers under SOLIDCRAFT_TEST_JOBS

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 11:50:10 +00:00
Victor Santos Oliveira a3c7a6ff93 CI: let the paused-work bot label pull requests (#6852) (#6884)
* claim: #6852 paused-work PR labels

* fix(ci): permit paused-work labels on pull requests (#6852)
2026-10-26 11:49:59 +00:00
Christopher Koch e6fb3793dc design: a whole-boundaries loop loads in any curve order (#6499) (#6516)
* claim: #6499

* Design: a whole-boundaries loop loads in any curve order (#6499)

---------

Co-authored-by: Framewrk CI <ci@framewrklabs.ai>
2026-10-26 11:29:00 +00:00
CamWight c6d361347f Section Analysis: the cap is marked by true depth, so far edges no longer take a cap band (#6328) (#6332)
With a section shown, the marking pass decided where the cap shows (the stencil) and wrote the
depth everything after it tests against. #1768 gave it the faces' FACE_BIAS for the second job,
so edges draw over their own faces (#1434). For the first job its slope part is unfair: next to an
edge where a back face seen through the cut meets a front face, the front face won a band about a
pixel wide although the back face is nearer. The cap skipped that band, and the edge line showed
through the cut.

#6363 marked those samples again from the back faces, pulled toward the eye as far as FACE_BIAS
pushes. That closes the gaps beside the edges, but the pull also lets back faces win wherever they
lie within the push of a front face, along every body's far silhouettes, and the cap hatches that
band. On bherbruck's block, the band away from the cut went from 58 px orthographic and 534 px
perspective before #6363 to 618 and 780 with it. In the app, his filleted hub showed it in every
frame of a 36-step orbit in both projections.

This replaces that pass. While a section is shown the opaque model now draws in four steps (`Cut`):
1. mark: the faces by their true depth (no push): colour, the stencil and depth;
2. a triangle over the viewport sets the depth back to the far plane, colour and stencil kept;
3. push: the faces again with FACE_BIAS, depth only, so the depth is what unsectioned frames have
   and edges draw over their faces as before;
4. cap: depth only, as #6728 made it; the mark pass has drawn the cap's colour and hatch, as fs_solid
   draws a back face seen through the cut.
The window pass's depth reset and this one are one helper, `depth_reset_pipeline`, for either
depth direction, and its passes are built by #6728's `solid_writes`. Unsectioned frames are unchanged. The CPU rasterizer already chose the cap by
true depth.

Tests (crates/gpu/tests/depth.rs), with the device's float depth and the 24-bit fallback:
- a_cut_hides_the_edges_behind_it (new): bherbruck's block cut at y = -16, home view, orthographic
  and perspective. No edge shows through the cap, and away from the cut the picture is a box of
  just the kept part's: 0 px orthographic and 1 perspective (main: 618 and 780).
- bherbruck's pocket and hub tests from #6363 are kept and pass (pocket 0 px; hub 1 and 2 px over
  its twelve views, as with #6363's pass).
- an_edge_on_a_face_draws_whole_at_any_angle and
  an_edge_just_behind_a_face_stays_hidden_on_a_large_design also run with a section shown.

In the app (safe mode, WARP), with 625 patterned cylinders cut by a section, the frame time is
that of main (#6363): 33.5 against 34.3 ms at the home view, 34.1 against 35.2 ms with the model
filling the view.

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:16:44 +00:00
github-actions[bot] a078d4e7e9 Regenerate generated files and fold pending files [skip ci] 2026-10-26 17:30:10 +00:00
github-actions[bot] c71234edfb Regenerate generated files and fold pending files [skip ci] 2026-10-26 17:24:19 +00:00
Jan Szumiec 00ff63f75e Face pick by point: linear work budget, so bodies with many faces take a sketch (#6720) (#6721)
* claim: #6720

* Face pick by point: linear work budget, so bodies with many faces take a sketch (#6720)

A face picked by its point alone charged the whole mesh once per face, and
face_contains scanned every triangle for each candidate. A block with a
modelled thread (about 5 000 faces) went past the 2 000 000 limit. The
triangles are grouped by face once, each face's test reads only its own,
and the budget charges triangles + faces.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:16:17 +00:00
leValdemar19 82cd5cc52e Sketch Dimension: value box in the document's units (#3812) (#3878)
* claim: #3812

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

* Sketch Dimension: value box in the design's units (#3812)

A dimension added without a value restated what it measured as
"<value> mm", whatever the design's units. In an inch design the
Dimension tool's value box therefore opened at "25.4 mm" next to a
dimension whose text read 1.000 (inches).

Document::design_length writes a measured length in the design's units
("1 in"), with enough decimals to keep it to the micrometre, so a
dimension that restates the geometry still moves nothing. The Dimension
tool, AutoConstrain's dimensions and Toggle Driven (making a driven
dimension driving) use it. Millimetre designs are unchanged.

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

* Move the design-units dimension tests into sketch_ux_tests (review)

The #3812 tests go in the existing sketch dimension test file rather than
a new file of their own (AGENTS.md, #3226).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:16:12 +00:00
Colin Faulkingham 7d7016b356 Insert Design: preserve source part properties (#6624) (#6691)
* claim: #6624

* Preserve source metadata when inserting a design (#6624)
2026-10-26 11:16:07 +00:00
Colin Faulkingham cd66495e14 Preserve plastic rules and hidden bodies through renames (#4421) (#4425)
* claim: #4421

* Fix body metadata and visibility across renames (#4421)
2026-10-26 11:15:31 +00:00
Colin Faulkingham 484f0cf8ae Sketch: keep nested circles ordered when diameters change (#6764) (#6789)
* claim: #6764

* fix(sketch): preserve nested-circle order during dimension edits (#6764)
2026-10-26 11:14:59 +00:00
SS-EN daecea6edf Evaluation cache: charge each state only for the bodies it made, so setting a parameter back restores (#6897)
The evaluation cache's 64 MB budget charged every cached feature result the whole model, though a
state shares every body it did not change with the state before it. A design whose model is a few
MB kept only its last few dozen results, so setting a parameter back recomputed what had dropped
out. Each result is now charged for the bodies and sketches it does not share with the state it
started from.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:14:57 +00:00
Homero Thompson bd38bc40be Sketch: the Midpoint Line tool shows the length and angle while drawing (#6198) (#6401)
* claim: #6198

* Sketch: show the Length/Angle readout while drawing a Midpoint Line (#6198)
2026-10-26 11:14:22 +00:00
Frank 19c8eb846e Crash reports: keep journaling through file.new and doc.open (#6550) (#6551)
* claim: #6550

* Crash reports: keep journaling through file.new and doc.open (#6550)

`Session::replace_design` swapped in the fresh session without its
`crash_journal` flag, and the app turns it back on only at its next frame.
So `file.new`/`doc.open` never recorded how they ended, the commands sent
after them in the same frame (a control-channel or MCP batch) were not
journaled, and a crash report then named `file.new` as the command in
flight. `replace_design` now keeps the flag, like the other session-wide
fields it keeps; only the design on screen has it set.

Test: `commands_right_after_a_new_design_are_journaled` (crash_tests.rs).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:14:20 +00:00
Dahak Builds 6f5f64f2d9 Viewport: perspective while orbiting, orthographic on face-on views (#2682) (#6700)
* claim: #2682

* WIP: perspective-while-orbiting with auto-ortho on face views

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 11:14:14 +00:00
Homero Thompson b2525ed34e Tests: the six timing assertions do not depend on the wall clock (#6917) (#6925)
* claim: #6917

* Tests: the six timing assertions do not depend on the wall clock (#6917)

Six tests asserted on the wall clock. Their result depended on how loaded
the machine was and not only on the code under test: they passed on an idle
machine and failed when the test binaries ran beside each other, which is how
the suite runs (`cargo xtask test` starts every test binary at once).

CONTRIBUTING.md:196 says it: "Tests run beside each other on a busy machine,
so a test must not depend on wall-clock timing."

The change is confined to the six test files the issue names:

- geom: the tangent chain of a helix is a ratio (750 pieces against 3 000)
  instead of a limit of 200 ms.
- io: the placement budget is spent inside the reader, so the clock is gone.
- ui-egui: the classification of many faces is a ratio (900 against 3 600)
  instead of a limit of 10 s.
- engine: the clearance measurement drops its 30 s limit; the boolean that
  gives up reports itself.
- engine: the fillet beside a modelled thread is a ratio (a 1 mm thread
  against an 8 mm one) instead of a limit of 30 s.
- ui-egui: the window frame budget goes from 250 ms to 20 s, the limit #6072
  gave its own probe.

The helper is the one #649 wrote, copied into the crates that need it, with
the noise of a run as a parameter.
2026-10-26 11:14:11 +00:00
Homero Thompson 72a76edcf7 Shortcuts: Ctrl+Shift+S opens Save As in the workspace (#6558) (#6567)
* claim: #6558

* Shortcuts: Ctrl+Shift+S opens Save As in the workspace (#6558)

The Home page already opened Save As with Ctrl+Shift+S, but the design workspace matched Ctrl+S, so the chord saved over the current file instead of offering another one. The workspace now routes the chord to Save As, and the File menu shows that shortcut next to Save As, taken from the command registry.
2026-10-26 11:14:07 +00:00
Colin Faulkingham 710b7a664e Save a mesh larger than the design file once it is compressed (#6600) (#6701)
* claim: #6600

Co-authored-by: Cursor <cursoragent@cursor.com>

* Save an imported mesh that only fits after compression (#6600)

The design file limit was applied to the mesh before Deflate, so a mesh
that had already loaded could not be saved or autosaved.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: validate and version large mesh saves (#6600)

---------

Co-authored-by: Colin Faulkingham <colinfaulkingham@Colins-Mac-mini.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 11:13:48 +00:00
HiroKenEz 0d32821e9e Top-level cut and join reach bodies in sub-components (#5679) (#6246)
* claim: #5679

* Top-level cut and join reach bodies in sub-components (#5679)

An extrude, revolve or press-pull from a sketch on a face of a body in a
component inside the active one now cuts or joins that body, moved or
turned, in its own component, so every placement shows the change. The
automatic operation probes that body where it is shown. A linked part, or
a face on a further placement of a part placed more than once, is refused
with the reason instead of a silent no-op (#5678).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:13:45 +00:00
Homero Thompson d9c5e4c325 Sketch: a circle drawn at the origin can be moved (#5034) (#6462)
* claim: #5034

* Sketch: a circle centred on the origin can be moved (#5034)

* test(#5034): say what the assertion really checks
2026-10-26 11:13:14 +00:00
Homero Thompson 12fe17d1b7 Modeling: a copied body keeps the appearance of its original (#6423) (#6456)
* claim: #6423

* test: a copied body must keep the look of the body it came from

solid.move with copy: true leaves the copy without the appearance, the
material or the face looks of the source body, so it renders with the
default look. The tests fail for that reason: the copy has no entry in
appearances.bodies, in materials or in appearances.faces.

* fix: a copied body keeps the look of the body it came from (#6423)

A move with `copy: true` pushed a new body but left it with the default look, so the copy came out a different colour than the part it came from. The copy now carries the body's appearance and its material, as the component copy already did.

* test: keep the copy regression to the body look and the material (#6423)

* test: each copied body keeps the look and the material of its own source (#6423)

Two bodies with different appearances and different materials, copied in one solid.move: each copy must keep the look and the material of the body it came from.

Scope of the fix: the look of the BODY and its material are carried over to the copy. The per-face looks are NOT carried over: a face look is keyed by the point on the face, so the copy's faces would have to be matched and their points translated, which is a separate feature.
2026-10-26 11:12:50 +00:00
semmelrinde cf7fbeaba1 Sketch drag: a scripted drag solves the steps past a failed one at once (#6606) (#6607)
A drag that isn't live walks to the cursor in steps, each a held solve
on its own budget seeded on the last solved state. Past a failed step
the walk keeps trying every step (a stretch the geometry can't follow
can be followed by one it can), and each failing solve runs the
numerical solver to its limit: one drag in a 65-command sketch took
7.7-9 s, most of it in failing solves.

A failed step leaves the sketch as it was, so the steps after it all
start from the same state and none changes what the next one sees.
They are now solved eight at once on all cores; those that fail in a
row are taken as tried and the walk goes on at the first that doesn't,
as it would one by one. The drag above takes 3.7-4.3 s and every
command's reply is unchanged. Live frames, whose steps share one
frame's budget, are left as they are.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:12:40 +00:00
semmelrinde e2c9f908f8 Sketch: an offset slot can still be moved by the dimension that places it (#6569) (#6697)
* claim: #6569

* Sketch: an offset slot can still be moved by the dimension that places it (#6569)

An offset arc is drawn round its source's own centre, and its ends are
held while the sketch solves (the offset is rebuilt from its source
afterwards). Its arc condition, both ends as far from the centre, then
only tied that shared centre, and the source arc with it, to the middle
of the held ends: a slot offset by 1 mm lost a degree of freedom, and
the length that places it was refused as conflicting.

The solver and the constructive reading now leave out the arc condition
of an offset arc round its source's centre (Sketch::offset_arc_follows_source),
as #4466 did for the Parallel between an offset line and its source. The
source's own arc keeps the centre; nothing changes without an offset.

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

* Sketch: a test for a rounded rectangle with an offset round it (#6569)

kaievns's case from the review: the offset's corner arcs tied each corner
centre to a 45 degree line, so the height followed the width and every
width change was refused.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:12:15 +00:00
NOWN-DEV fa961eaa31 Sheet metal: a pattern of a cut made in the flat is kept like the cut (#6841) (#6853)
* claim: #6841

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

* Sheet metal: a pattern of a cut made in the flat is kept like the cut (#6841)

A rectangular, circular or path pattern of an extrude cut through sheet metal was refused
("this sheet machining cannot be preserved in its flat material"), though the cut itself is
kept as a cut-out of the flat pattern. The pattern replayed its copies through the generic
boolean, whose sheet check refuses any change the sheet model can't keep; the cut goes through
the sheet's flat machining first.

Each copy of an untapered extrude cut is now machined into the sheets the same way, in its
moved sketch plane (a moved copy keeps the extrude's prism certificate), and then cut from the
other targets. A copy that meets a folded bend is refused as the cut is there. Mirrored copies
(the sketch plane turns over) and patterns of an intersect (which cut with the union of their
copies) keep the generic path.

Fixes #6841.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:12:12 +00:00
sascha2101 aa88360357 Sketch: deleting part of a projected link no longer brings it back (#5098) (#6324)
* claim: #5098

* Sketch: deleting part of a link breaks off the rest instead of rebuilding it (#5098)

Deleting some of a link's curves left the rest linked. The next refresh saw a
different layout and rebuilt the whole link as normal geometry: the deleted
curve came back and the others lost their construction flag (a projected arc
returning as a solid arc, a projected face's lines turning solid).

sketch.delete now breaks the link for what remains, as Break Link does: those
curves stay where they are and keep their flags. Deleting a whole link, and
text links, are unchanged.

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

* Move the #5098 tests up in sketch_project_tests.rs

They sat at the end of the file, where main has since added other
tests; moving them next to the break-link test avoids the merge conflict.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:11:19 +00:00
sascha2101 e1ff3fa9c0 Batch: refuse a step that would fail at its turn before the first step runs (#5853) (#6781)
* claim: #5853

* Batch: refuse a step that would fail at its turn before the first step runs (#5853)

engine.batch and the MCP batch tool checked only a batch's shape up
front. A misspelled command id in a late step was found at its turn,
after every earlier step had run, and then all of it was rolled back: a
160-step batch with a typo as its last step ran for a minute and
changed nothing.

solidcraft_engine::batch_preflight::check finds, before anything runs,
the first step the batch would certainly refuse at its turn: a step
without a command, an unknown command id (the error names the closest
ids, as at the step's turn) or, with strict parameter checking, a call
that breaks its command's typed contract. Anything that depends on the
design is left to the step's turn.

- engine.batch (control channel) checks against the app's registry and
  its session's level; view.pick, which the app answers itself, passes.
- The MCP batch tool checks when the commands run in its own process
  (headless, with --param-check from the backend). A running app may be
  another version with other commands and checks the batch itself; its
  reply's not_run is passed on.
- The refusal is the failing step's reply with completed 0, undone 0,
  not_run listing every step and a note.

Tests that used an unknown command only to make a late step fail now
use a step that fails at its turn (a bad extrude distance), so they
still test rollback, timings and file.new as before.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:11:10 +00:00
yhamil04 3afcd69a5c Joints: a loop whose axes are a hair out of parallel still moves (#6211) (#6602)
* claim: #6211

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

* Joints: a loop whose axes are a hair out of parallel still moves (#6211)

Holes picked on parts are rarely exactly in line. A closed loop through
axes a few hundredths of a microradian apart (a lever, link and a rod on
a cylindrical axle) can't close exactly: it closes to nanometres, and the
gap grows as it turns. The loop solver wanted 1e-7 mm, so it threw such a
closure away (the loop stayed open, its parts at 0 dof) or stopped a drive
or drag after a fraction of a degree.

The conflict check is now 1e-4 mm / 1e-6 rad (it was below what its
message can show), and joint.drive / motion and joint.reach close the
loop at each step as at the start: to the tight tolerance where they get
there, otherwise within the conflict check.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:10:45 +00:00
yhamil04 a3483ed451 Body rename: an imported assembly's other bodies keep their names and components (#2175) (#6615)
* claim: #2175

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

* Body rename: an imported assembly's other bodies keep their names and components (#2175)

An Import keeps its body names by the file's body index, and builds no body for the
file's later instances of a repeated part (`skip`). Renaming a body stored the names by
the body's place in the model instead, so every name after the first skipped instance
landed one slot early (and the new name could land on a skipped slot). The rename looked
right until the design was rebuilt; then those bodies took other names, dropped out of
their components and showed up loose in the import's top component.

`Feature::body_name_slots` maps the bodies a feature builds to their stored slots, and
`Feature::pin_body_names` stores current names through it. The rename, the pinning of
later features' names (rename and suppress) and `Document::body_rename_from` use them.

Fixes #2175

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:10:39 +00:00
yhamil04 d7252f2240 Sketch: constrain to an edge of any body, a mirrored copy too (#6348) (#6617)
* claim: #6348

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

* Sketch: constrain to an edge of any body, a mirrored copy too (#6348)

A constraint tool ignored a click on a body edge, so only edges already in
the sketch (the face it sits on) could be constrained to; a mirrored copy's
edges never were. Constraint commands now take a model edge or vertex
({edge|vertex: [x,y,z], body?}) for an operand and project it first, in the
same undo step, and the pick tools pass a clicked edge or vertex that way.
The link keeps the edge's persistent name, so the constraint follows rebuilds.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:10:35 +00:00
raykholo 30e9b87b48 Render workspace uses the window's graphics backend on Windows (#6592) (#6629)
* claim: #6592

* Keep Windows Render setup on the viewport backend
2026-10-26 11:10:32 +00:00
github-actions[bot] b20f522f5d Regenerate generated files and fold pending files [skip ci] 2026-10-26 16:34:07 +00:00
Mateusz Sych d895fef354 Control channel: a pushed design.changed event for clients that subscribe (#6985) (#6986)
* claim: #6646

* Control channel: document.revision, a design revision and a document id (#6646)

A program that follows the open design asks document.revision and exports
only when the answer moves: document_id names the open design (kept through
edits, saves and renames, another for each design started or opened, never
shared between two tabs or two runs) and revision moves whenever the design,
what is hidden or an uncaptured component position changes. It stays put on
reads, selections and view changes, unlike the session's view revision.

The revision is read lazily: the session remembers the design it last
reported (held weakly, so Arc::make_mut can never change it in place and its
address can't be reused) and takes a new number from one counter for the
whole run when the design is another one. The control method answers at once,
also while a slow command computes; engine.execute reaches the same command
for scripts and MCP.

* document.revision: hash the run's id prefix with the process's random keys

A clock set before 1970 gave every run the prefix 0, so a restarted app could
answer a new design with the id and revision of the old one. The docs no
longer promise an answer at once: a screenshot waiting for an opened design to
be meshed still holds the requests behind it.

* document.revision: its label in the complete catalogs (ja, zh)

* claim: #6985

* Control channel: control.subscribe and design.changed events (#6985)

A connection that sends control.subscribe gets the active design's id and
revision in the reply (as document.revision answers), then a
{"event":"design.changed","params":{document_id, revision}} line whenever
either changes, so a live link need not poll.

The window publishes the design at the end of each frame's logic and
drawing while a connection is subscribed (one atomic load otherwise). Each
subscribed connection waits on that from a thread of its own and writes
the latest after a 100 ms gap, so a burst of changes is one line. Replies
and events share one lock per connection, each written whole (#6390).

* control.subscribe: the reply before any event, a second one refused

The events thread started before the subscribe reply was written, so a
change right after the window's answer could reach the client before the
reply that named the older revision. The subscription now starts holding
the connection's line lock and writes the reply under it.

A second control.subscribe on a subscribed connection is refused on the
connection itself: its answer could arrive behind a newer event. When no
thread or socket is left for the events, the reply says it did not
subscribe instead of ok.

* control.subscribe: a second one does not lose a finished late reply

The already-subscribed answer came before the busy check, so a request that timed out and had finished since lost its reply: control.pending then said the app ended it without one. It now comes after the busy check and drops a finished late reply as every other request does.

* control_server: declare the subscribe guard at the top of the loop

Clear of the lines #6643 changes (its token check moves to where the declaration was).

* control_server: the subscription's items beside serve

Away from the lines next to send, where #6643 adds token_of.

* control test: New on a blank design reuses its tab, so redo the box first

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-26 11:10:03 +00:00
Dahak Builds 070b1414ee Fix manual vertical/horizontal constraints with circle centre (#617) (#6928)
* claim: #617

* fix(sketch): manual Horizontal/Vertical between a circle centre and an edge (#617)

The manual H/V tool silently did nothing when a point and a straight line
were picked or selected together (only two points or a lone line worked),
while auto-constrain inference applied the alignment. A point picked or
selected with a straight line now aligns the point with the line's
midpoint, in the mode the line's dominant axis suggests, through the
existing midpoint alignment in sketch.constraint.horizontal_vertical.

- tools: constraint_preselection consumes [point, line] selections; the
  interactive tool applies the alignment on the second pick (a line).
- context menu: Horizontal/Vertical Constraint items for point + line.
- engine + ui regression tests pin centre-edge alignment and the
  edge-to-centre dimension from #617.

Written by Qwen.

* style: formatting cleanup for #617 files

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 11:09:52 +00:00
n0tfr3shm1lk 111c662fe4 Extend: join a stop that is already another curve's end (#6863) (#6865)
* Extend: join a stop that is already another curve's end

sketch.extend refused a target that coincided with another curve's end
("the extension target coincides with existing geometry") instead of
extending onto that point. The stop is now joined: a new line ends on the
existing point, and a free end grows onto it held by a Coincident. The
preview shows that stop too, so the line it draws is the line the click adds.

A stop at the end of a spline or conic joins that end the same way, and
extending an infinite line now refuses instead of moving one of its two
points.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Extend: close an arc, take an equal stop, preview with the command's search

An arc with nothing on its path now grows all the way to its own far end and
closes into a whole circle instead of reporting that there is nothing to
extend to.

Two curves stopping the extension equally far are both on the way, so the first
stop is taken instead of refusing to choose between them.

The line preview and the command now share one stop search (`line_aim`), so the
preview can no longer draw a line the click refuses.

---------

Co-authored-by: n0tfr3shm1lk <n0tfr3shm1lk@ukr.net>
Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-26 11:09:42 +00:00
Dahak Builds ab709e6c46 Add base-height and new-body options to Snap Fit (#1994) (#6911)
* claim: #1994

* plastic.snap_fit: base_height and new_body options (#1994)

Base Height extends the snap fit's base below the picked face; New Body
makes the snap fit its own body so it can be patterned or moved. Both
optional, serde-defaulted, old designs replay identically.

* ui: snap_fit length-field defaults list gains the optional base height

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 11:09:30 +00:00
Colin Faulkingham 03bf992bc1 Keep linked components valid through Undo after Save As (#6860) (#6877)
* claim: #6860

* fix: preserve linked component targets across Save As history (#6860)
2026-10-26 11:08:32 +00:00
bherbruck 586a358ed4 Release 0.23.1 2026-10-26 07:07:21 -04:00
bherbruck 4d61100534 Release 0.23.1: version bump, design format 38 (#7035)
The component colour tests no longer assume a design always carries a list of
pending format changes; once the release folds them, there is none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:07:18 -04:00
tr1pp4 25ad6c7266 Browser: a feature's menu edits its profile sketch, suppresses and rolls back (#6673) (#6674)
* Browser: a feature row edits its profile sketch, suppresses and rolls back

A sketch button on feature rows with a profile sketch, and the feature menu gains
Edit Profile Sketch, Suppress/Unsuppress and Roll Back Here. Features behind the
history marker or suppressed are drawn faint, as in the timeline.

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

* Browser: the sketch button sits right after the feature's name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:05:38 +00:00
HiroKenEz cfb3f52aec cli_link: the symlink test runs on Unix only, so the UI tests build on Windows (#6545) (#6546)
* claim: #6545

* cli_link: the Unix-only link code and its test build on Unix only, so the UI crate builds on Windows (#6545)

slot() is used only by link_in's Unix version, and links_only_where_it_may
makes its own link with std::os::unix::fs::symlink; gate both like link_in.
On Windows the test failed to compile (E0433) and clippy -D warnings flagged
slot as dead code.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:46:43 +00:00
Colin Faulkingham 7c31631ea6 Draft: preserve the selected faces’ body (#6350) (#6446)
* claim: #6350

* fix(ui): preserve the selected faces' body in Draft (#6350)
2026-10-26 10:45:09 +00:00
Frank c13e516cd5 fix: allow SpaceMouse rotation after tilting sketch view (#6687)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 10:44:31 +00:00
raykholo c264c7a42a Startup falls back from a Direct3D 12 that can't build the UI pipeline (#6648) (#6682) 2026-10-26 10:44:13 +00:00
HiroKenEz 56f09f3993 Frame rate readout stays beside a right-side pane, not under it (#6755) (#6767)
* claim: #6755

* Frame rate readout stays beside a right-side pane, not under it (#6755)

The readout and its details sat in the viewport's bottom-right corner even under the
floating Assistant. They now move just left of any pane over that corner and come back
when it closes.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:43:53 +00:00
Dahak Builds 6e63f8bd97 Construct toolbar regression tests: dialogs ask for picks (#204) (#6681)
* claim: #204

* Construct toolbar regression tests: dialogs ask for picks (#204)

* chore: re-trigger checks (defaults line already present in PR body)

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-26 10:43:47 +00:00
Christopher Koch b01f361edf AI chat: keep only the newest views in the history, and explain HTTP 413 (#2298) (#2299)
Every view attached to an API-agent message stayed in the history as a
base64 data URL, and the whole history goes with every request (up to 24
per message). Requests grew by one screenshot per message until the
endpoint refused them with HTTP 413, after which every message failed.

Keep the newest two views as pictures and swap older ones for a short note;
the text sent with them and every other message stay. HTTP 413 now says the
conversation is too large for the endpoint and to start a new chat.

Fixes #2298

Main now speaks the Anthropic protocol too, where a view is an `image`
block rather than an OpenAI `image_url` part: drop_old_views trims both,
before trim_history. ui.ai_http_too_large is in every catalog that has the
other ai_http_* messages.

Co-authored-by: Framewrk CI <ci@framewrklabs.ai>
2026-10-26 10:43:30 +00:00
Colin Faulkingham c482dab4a1 Sheet Metal: open the export save picker (#6847) (#6856)
* claim: #6847

* fix(ui): open sheet export save pickers (#6847)
2026-10-26 10:43:01 +00:00
raykholo 189b47c750 Chamfer and Fillet refuse a point where several edges meet (#5173) (#5177) 2026-10-26 10:42:59 +00:00
Colin Faulkingham 1530a8e247 Route left History feature clicks into command dialogs (#6532) (#6734)
* claim: #6532

* Fix History feature picks in command dialogs (#6532)
2026-10-26 10:42:49 +00:00
sascha2101 1d2674ebc4 Sketch: a regular line on a construction line is picked before it (#6396) (#6753)
* claim: #6396

* Sketch: a regular line on a construction line is picked before it (#6396)

Coincident sketch curves tied on pick distance and the one drawn first
won, so a construction line drawn first hid the regular line on part of
it. Construction and centre lines now get half a screen point added to
their pick distance: on a tie the regular curve is hovered and picked,
and a construction line clearly nearer the cursor still wins. Clicking
again and Select Other still reach the construction line.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:42:48 +00:00
momysnow 10b5cc5914 GPU: an uncaught error on the window's device is logged, not a panic (#5486) (#5667)
wgpu's default uncaptured-error handler panics ("wgpu error: Out of Memory").
The headless picture devices already report instead; the window's device had
no handler. Errors the code expects stay under error scopes.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 10:42:44 +00:00
Kai Evans 41a938e731 Sketch: a fillet stays the arc of its corner when the corner mirrors or a line is dragged through it (#5677)
A line tangent to an arc at the arc's end had no sense: the tangency held
with the line on either side of its touch point, so pinning a line's far
end across the corner left the line cutting back through its fillet, and
when the solver found the mirrored corner the arc kept its stored
direction and became its three-quarter complement, inside out.

The solver now keeps each fillet an arc of its corner. A line tangent at an
arc's end keeps its sense there (it leaves the arc as it did) and the arc
its side of its chord, compared with the positions before an interactive
move or the stored ones. A corner that mirrored gets its arc on the
mirrored side: its ends swapped where the solve ran the arc round the
other way with the lines following, or the arc reflected across the line
that stayed and solved again where a line cut back through. A solve that
would loop the arc the other way round, or fold a line onto it, is refused
as a fold is. A drag's cursor-following steps keep the fillets too, so a
line dragged through its own fillet stops at the arc.
2026-10-26 10:42:41 +00:00
N3MI-DG 9d0b509961 ui_kit slider: cover egui's own handle at the top of the range (#5708) (#5712)
The kit slider adds a real `egui::Slider`, lets egui paint it, then paints an
opaque rectangle over it and draws the kit's track and thumb on top. That mask
was exactly `slider_width` wide, and egui places its handle so the circle's
right edge lands on `rect.right()` at the maximum, so the fill stopped flush
with the handle and its last column survived as a light sliver beside the
value box. The kit's own thumb cannot hide it either: its radius is
`t.space_8` while egui's handle radius is `input_radius`, about two pixels
larger.

Extend the mask one pixel past its right edge. The gap before the editor is
`item_spacing.x`, so nothing else is covered, and only the maximum was
affected: at the minimum the handle's left edge coincides with the mask's left
edge, which a fill does cover.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-26 10:42:38 +00:00
sascha2101 7ae17e6dd5 Sketch display: each sketch samples within its own budget (#6524) (#6838)
* claim: #6524

* Sketch display: each sketch samples within its own budget (#6524)

After a large DXF was inserted, lines drawn in a later sketch vanished
once finished: only their end points showed and could be picked, and
hiding the DXF sketch brought them back.

The display cache had one sampling budget for every shown sketch (4096
curves, 131 072 vertices per model state and view). Sketches are drawn
and picked in timeline order, so a large imported drawing used it all
and every later curve came back empty (BudgetLimited): not drawn, not
pickable, while the points, drawn separately, stayed.

Each sketch now has its own budget with the same limits (curves,
profiles, vertices and nodes). The cache as a whole is capped at 65 536
curves and 16 384 profiles, so memory stays bounded however many
sketches are shown.

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

---------

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:42:35 +00:00
github-actions[bot] 1cd14e2412 Regenerate generated files and fold pending files [skip ci] 2026-10-26 14:29:06 +00:00
github-actions[bot] 7014b62388 Regenerate generated files and fold pending files [skip ci] 2026-10-26 14:22:28 +00:00
HiroKenEz adc568a715 Context menu: the Appearance flyout closes when the pointer moves to another row (#6657) (#6784)
* claim: #6657

* Context menu: the Appearance flyout closes when the pointer moves to another row (#6657)

Hovering Appearance opened its quick-choice flyout, but resting on a plain
row of the same menu never closed it: only another submenu row replaced it.
Now a flyout closes once the pointer moves onto another row and rests there
for 0.3 s, so a diagonal path to the flyout across a neighbour keeps it open.
A flyout opened with the keys stays open while the pointer is still.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:42:32 +00:00
Colin Faulkingham 12b0d9dbff Preserve unchanged results when rolling the timeline (#6008) (#6067)
* claim: #6008

* Reuse rolled-back timeline results after cache eviction
2026-10-26 10:42:32 +00:00
semmelrinde 613903229e Patterns: copies of a To Object extrude follow the face it reaches (#2121) (#6093)
A feature's face points are moved onto their named faces before it is
evaluated, so a To Object extrude follows its target face when an earlier
feature moves it. A pattern's or mirror's copies were built from the
feature as stored, with the face where it was picked: after a gear's disc
was made thicker, the tooth reached the new top but its pattern copies
kept the old height. The copies now use the same moved points; the move
is one helper used in both places.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:42:28 +00:00
Jesper We 0c4eb458df macOS 3D mouse: unregister from the driver when the app ends, so the next launch gets the mouse (#6114) (#6115)
* claim: #6114

* macOS 3D mouse: unregister from the driver when the app ends, so the next launch gets the mouse (#6114)
2026-10-26 10:42:25 +00:00
Homero Thompson b00c432ac9 space-mouse: cover the 32-bit usage item (#6138)
Issue #37
2026-10-26 10:42:21 +00:00
Jesper We 8a28d13161 Perspective view as a command, so a 3D mouse button can switch it (#6143)
* claim: Perspective view as a command

* Perspective view as a command, so a 3D mouse button can switch it
2026-10-26 10:42:18 +00:00
NOWN-DEV 74321f8bbd Sketch Intersect: crossings on curved faces lie on the surface; a plane on a top face cuts it (#6272)
* Test: sketch Intersect keeps its exact cuts, crosses curved faces on the surface, cuts on a top face

Intersect's cut of a box, a cylinder, a sphere and a tilted cylinder, as it is on main, pinned.
Two cases where it is not exact: a plane through a cylinder's rim cuts the mesh's chord (its sides
come out at 6.3159 and 6.3085, not the square root of 40; a hole's walls at 25.0122), and a plane
on a body's top face finds nothing ("projects to nothing on this sketch plane").

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

* Sketch Intersect: crossings on curved faces lie on the surface; a plane on a top face cuts it

Problem: the plane cut that sketch Intersect makes of a body's mesh (project::section) took a
crossing on an edge where a curved face meets another (a cylinder's rim on its cap) from the
merged normals, which are undefined at the crease, so it fell on the chord: a cylinder's sides came
out 0.01 mm off and uneven. A plane through a vertex or on a face counted the vertices on it as
below, so a plane on a body's top face crossed no edge and found nothing.

Solution: the crossings are found by section_segments, which section fits as before: an edge where
a curved face meets another takes the curved face's normals, so the crossing lies on its surface;
a vertex within 1e-9 x the mesh's size of the plane is on it, on one side for every edge (below
when the mesh lies on or above the plane, else above), and an edge ending there crosses at that
vertex, which is an exact point for the fit. Each segment keeps its B-rep face, for a section that
groups them by face. Cuts that were exact are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:42:08 +00:00
Colin Faulkingham a67b3bfdf3 Surface Loft: retain profiles from multiple sketches (#6400) (#6844)
* claim: #6400

* fix(ui): retain surface loft profiles across sketches (#6400)
2026-10-26 10:42:05 +00:00
Colin Faulkingham abdf9d79cf Move/Copy: show a handle for the selected rotation axis (#6822) (#6832)
* claim: #6822

* fix(ui): show selected-axis handle for Move Rotate (#6822)
2026-10-26 10:42:01 +00:00
Colin Faulkingham b1bbca97ef Translate the remaining history tooltip text (#4370) (#6905)
* claim: #4370

* fix(ui): translate history tooltips and feature status
2026-10-26 10:41:50 +00:00
Homero Thompson 2ec11cfc3a Docs: how to open the macOS build when the Gatekeeper blocks it (#5393) (#6566)
* claim: #5393

* docs: how to open the macOS build when the Gatekeeper blocks it (#5393)
2026-10-26 10:41:32 +00:00
github-actions[bot] 2205955329 Regenerate generated files and fold pending files [skip ci] 2026-10-26 13:52:28 +00:00
bherbruck bd9d0778f6 CI: keep lavapipe's shader cache for the GPU path tests (#7113)
The first test in each GPU path shard compiles the path pipeline cold
on lavapipe (335-502 s on 16 vCPU, 665 s on 8, against the 600 s setup
budget); the later ones read Mesa's disk cache in about 30 s. Put that
cache on a sticky disk that main commits, so the first test reads it
too until the shaders change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:49:34 -04:00
cronos a0f7529fd6 fix(measure): select imported mesh outlines and endpoints (#7033) 2026-10-26 10:40:41 +00:00
Lars Jansen 4a0d519727 View: OK and Cancel in the corner of the view while a dialog or sketch is open (#6709) (#6714)
* claim: #6709

* feat(view): OK and Cancel in the corner of the view while a dialog or sketch is open (#6709)
2026-10-26 10:40:36 +00:00
Kai Evans ffbcdc614c History and parameter changes: the run here takes every result computed ahead (#7002)
A change computed off the window thread ran here with only the results of the evaluation
ahead's last evaluate primed. A parameter change evaluates twice (before and after its sketch
stores the new solution), so its first evaluate here found nothing to take, and on a design whose
states pass the evaluation cache's budget it recomputed the features on the window thread: the
window froze for seconds after Computing ended. The run now also takes what the evaluation ahead
kept (KeptResults, as an agent's control requests already do).
2026-10-26 10:40:33 +00:00
Thosch 06af6e1107 Pattern dialogs: Quantity is the count in every language, the motion plot keeps its own entry (#6538) (#6733)
The catalogue entry Quantity served both the pattern dialogs' number of
instances and the motion study's plotted quantity. Nine locales had
translated the second meaning, so the rectangular pattern dialog showed
e.g. "Größe" above "Anzahl 2" in German.

The motion study's row now reads motion.plot_quantity (a pending file
that keeps every locale's previous word there). Quantity takes each
catalogue's own ui.quantity text, which the "Quantity 2" row already
shows; nothing is newly translated. The glossary lists Quantity under
words with more than one meaning.

Fixes #6538

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:40:26 +00:00
bherbruck 84b1aeecae CI: GPU path tests in four shards on 16 vCPU (#7096)
On 8 vCPU the first merge-queue run after #7066 failed: one gpu path
test went past the 600 s pipeline-setup budget (665 s; 204 s for the
same test on the PR run) and shard 2 took 35 minutes. lavapipe compiles
on every core, so the shards move to 16 vCPU, and four count partitions
(27 tests each) replace three hash ones (41/40/27).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:23:41 -04:00
bherbruck a6354dfec5 CI: GPU path tests in three shards of their own; Windows in the queue only when a group touches it (#7066)
The 108 gpu path:: tests run one at a time (gpu-path test group) and took
about 36 of Native Linux tests' 45 minutes in a merge-queue run. They now
run in a GPU path tests job, three hash partitions on 8 vCPU, beside the
other tests; Native Linux tests leaves exactly them out. A `gpu` output of
Select checks drives both the job's `if` and the `ci` check.

The Windows jobs now run in a merge group only when its diff reaches what
they exercise (the pull request patterns and the Cargo.lock graphics-stack
check), not on pushes to main, and still nightly and by hand.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:22:53 -04:00
sascha2101 8108646e2c Docs: a sketch family's shortcut starts the variant used last (Refs #6630) (#6817)
Since #4308, L, R and C start their family in the variant used last, as
the toolbar button does, so L can start a Midpoint Line. The shortcuts
table still read "L | Line". Say so under the table and in the
sketching guide's Geometry section, with the preference that steps
through the variants.


Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq

Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:39:51 +00:00
Victor Santos Oliveira b3cbbe7bd5 CLI: reject incomplete and unsupported run/eval options (#6918) (#6919)
* claim: #6918 validate run and eval options

* fix(cli): reject invalid run and eval options (#6918)
2026-10-26 10:38:56 +00:00
Mishung ae2d2145b2 CONTRIBUTING: the desktop app runs on macOS too (#4535)
The README and docs/downloads.md ship a universal macOS disk image, but the
contributing guide listed only Windows and Linux.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:24:50 +00:00
Basil a800a40099 Fix Snap smoke startup race before viewport readiness (#5346) 2026-10-26 10:24:40 +00:00
Badger B dc3061a039 Docs: on Windows, give claude mcp add the CLI's full path (#5895) (#5896)
* Docs: on Windows, give claude mcp add the CLI's full path (#5895)

The Windows installer puts solidcraft-cli.exe in C:\Program Files\SolidCraft but not on PATH,
so the README's and docs/mcp.md's `claude mcp add solidcraft -- solidcraft-cli mcp` registers a
server that can't start there. Say so next to the command and give the full-path form.

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

* Docs: word the Windows full-path note so it holds once the installer sets PATH (#5895)

#5898 makes the MSI add its folder to PATH. Say "if a terminal doesn't find solidcraft-cli
(older Windows installers don't put it on PATH)" instead of stating that the installer never
does, so the note stays true whichever change lands first.

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

* Docs: the Windows CLI path is the default, not the only place (#5895)

The MSI installs to ProgramFiles6432Folder and lets people choose the folder
(WixUI_InstallDir), so C:\Program Files\SolidCraft\ is only the x64 installer's default; the
x86 MSI on 64-bit Windows lands in C:\Program Files (x86)\SolidCraft\. Say "by default" in
the README and docs/mcp.md (review on #5896).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:24:34 +00:00
Thosch 102314dbb2 Docs: blurry interface on Linux with display scaling, and how to fix it (#5979) (#6195)
SolidCraft runs through XWayland whenever an X display is available. A
compositor that doesn't let X11 apps scale themselves draws the window at
100 % and stretches it. docs/nix.md said so only for fractional scaling,
but GNOME 47 (RHEL and Rocky Linux 10) does it at any scale: the report
shows 1.05 physical pixels per point at 200 %.

The troubleshooting page gets a section with the cause, the Window system
preference, SOLIDCRAFT_WAYLAND=1 for 0.21.0 and earlier, and the file drop
trade-off; docs/nix.md links to it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:24:30 +00:00
Homero Thompson 4ced1d7bcf Viewport: the grid fades with the distance instead of cutting off (#4214) (#4629)
* claim: #4214

* Viewport: the grid fades with the distance instead of cutting off (#4214)

The ground grid stopped at a fixed distance from the view, so its edge showed
as a cut across the picture. It now fades out with the distance from the
middle of the view, and it is gone before either of the two lines that used to
cut it: its own edge, and the picture's far plane.

Each grid line is drawn in pieces, and each piece is as faint as the fade
makes it at the distance where it lies. The 'under' batch is drawn by the
overlay pipeline with alpha blending, so the colour's alpha reaches the
frame.

* The headless grid fades with the distance too (#4214)

The headless path (solidcraft-cli snapshot, the home-page thumbnail, the MCP
screenshot) drew the grid from the engine's palette, where a line had no
alpha: the grid still ended in a cut, and at a low camera angle the cut was
in the picture.

A SceneLine now carries an alpha, the CPU rasterizer blends with it and the
GPU capture passes it on, so the engine's grid can fade. The fade itself
moves to the engine, next to the grid's other geometry, and both paths use
it: the headless grid now reaches as far on the ground as the view is wide
(the ground stretches as it is seen at a slant) and dissolves into the
distance instead of ending in a line.

* Fix the headless/viewport grid fade divergence (#4214)

The engine stretched the grid's size with the slant of the plane it is seen at, the viewport stretched it only on the ground, so on a sketch's plane the picture's grid reached further than the window's (153.42 mm against 120.00 mm). Both now share one grid_ext, which the picture and the window draw the same grid from.

* The grid fades out before the picture's far plane cuts the ground (#4214)

The window's grid faded against a far plane of its own: the scene's radius
clipped by the projection's camera (260 mm away at 400 mm of distance), while
the picture itself cuts the ground at the far plane of its own projection
(223.7 mm at the same distance, because the projection frames the scene's
bounds and not its radius). The grid was still a third of its strength where
the picture cut the ground, so the cut showed as a straight line across the
view.

The fade now ends at the far plane the picture's projection uses, and the
grid is gone before that plane cuts the ground.

The test that watched the fade now measures the ink of each row of the
picture, and checks that the grid is already gone in the ten rows above the
row where the far plane cuts the ground: the point it compared before lay
beyond that plane, where both renders draw nothing at all, so the comparison
was always true.
2026-10-26 07:59:56 +00:00
DuBearte a82f839255 Docs: don't offer winget install before the package is in winget-pkgs (#5734) (#5827)
* claim: #5734

* Docs: don't offer winget install before the package is in winget-pkgs (#5734)

The README and docs/downloads.md said Windows users could run
`winget install bherbruck.SolidCraft`, but winget-pkgs has no manifests for
bherbruck.SolidCraft (manifests/b/bherbruck doesn't exist), so winget finds
no package. winget.yml only updates a package that is already listed, and
does nothing until then. Drop the command from both pages, and say in
docs/releasing.md that the first version has to be submitted once by hand
and the command added back when it's listed.

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

* Releasing: a first WinGet submission is already open; take it up (#5734)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:39:29 +00:00
Brandon Wees 08484af293 Finish the sketch when Extrude, Revolve or another feature starts (#2802) (#2815)
* fix(ui): finish the open sketch when a modeling feature starts

* fix(ui): wait for the marker roll before starting a feature from a sketch

* fix(ui): start the feature once the marker is back

* test(ui): keep the sketch open in the palette clearance test
2026-10-26 06:56:07 +00:00
raykholo d29e70bad9 docs: Save As blocked by Windows Controlled folder access (#4490)
Saving into Documents fails with the Windows dialog's "File not found" when
Controlled folder access blocks the app, and the message gives no hint.
Refs #4479.
2026-10-26 06:49:53 +00:00
Logan Night 1d482aff74 Docs: AppImageLauncher 2.x can't start the AppImage; update to 3.0 (#4640) (#4674)
* claim: #4640

* Docs: AppImageLauncher 2.x can't start the AppImage; update to 3.0 (#4640)

AppImageLauncher 2.x intercepts every AppImage through binfmt_misc and
can't start static-runtime AppImages such as SolidCraft's: it fails with
"execv error: No such file or directory" after a FUSE message. 3.0 starts
it. One paragraph in the Linux AppImage section says so.

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

* Docs: AppImageLauncher 3.0 is so far only published as betas (#4640)

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-26 06:49:52 +00:00
raykholo 2445c0f13d Flatpak repository: always publish its HTTPS URL (#4864) (#4875)
GitHub Pages reports the base URL as http:// for the custom domain, so the
.flatpakrepo file, the remote-add line on the repository page and the mirror
of the previous repository all used plain HTTP. Force https:// once where the
base URL is read.
2026-10-26 06:49:50 +00:00
Homero Thompson 3d0e2a8b3c docs: the gate reads the working tree, and how to show a picture without committing it (#4902)
* docs: two notes for agents and contributors (the gate's working tree, and linking a picture)

* docs: how to show a picture without attaching it

The reviewer asked for the pictures to be attached to the PR (or in a gist),
not hosted on a branch of a fork, so the note no longer teaches the branch
trick.

* docs: say how to attach a picture, and what a gist is for

The review of this pull request pointed at two things in the note on
evidence:

- Attaching works in the browser for anyone who can comment: drag the
  file into the comment. The limit is the command-line agent, whose
  upload needs write access to this repository. Say that, instead of
  claiming that a contributor without write access cannot attach a file.
- A gist suits a log or another text file; it does not render a picture
  inline. Say what it is for, so nobody hits that dead end.

The note in AGENTS.md about the gate reading the working tree is
unchanged.
2026-10-26 06:49:48 +00:00
raykholo 2a01cca00e User guide: local models with the Assistant (#4851) (#4964)
* claim: #4851

* User guide: local models with the Assistant (#4851)

A page on choosing and setting up a local model (Ollama, LM Studio, or any
OpenAI-compatible server) for the Assistant: tool support, VRAM, permissions,
the measured results from the standard test in #4851, and troubleshooting
for the common failures.

* Guide: local models table with the full set of measured results
2026-10-26 06:49:46 +00:00
raykholo d7cc5621fa Troubleshooting: what to do when a design won't open (#5114)
Point to the `<file>.bak` copy every save keeps, the design.solidcraft in crash
reports, the recovery folder for unsaved designs, and how to attach the file
to an issue.
2026-10-26 06:49:44 +00:00
raykholo be6c7fc17c Bug template: ask to reproduce on a current build first (#5150) (#5154)
* claim: #5150

* Bug template: ask to reproduce on a current build first (#5150)
2026-10-26 06:49:42 +00:00
raykholo f006eefd6e docs: point AGENTS.md's starting steps at public files (#6049)
"Start every session here" sent readers to plan/STATUS.md,
plan/execution-plan.md, plan/architecture.md and
plan/adr/0001-geometry-kernel.md. plan/ is gitignored, so nobody who
clones the repository has those files.

Point the steps at what ships instead: ROADMAP.md, CONTRIBUTING.md's
"Where help is wanted most", "How the code is laid out" and "Kernel
work is paused" sections, docs/architecture.md, the help wanted, good
first issue and needs-triage labels, and the licence rules with
deny.toml and cargo xtask licences --check.

Signed-off-by: Ray <raykholo@users.noreply.github.com>
2026-10-26 06:49:38 +00:00
bherbruck 15269f85a1 docs: how to bump the Homebrew tap on a release (#6276)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 06:49:37 +00:00
bherbruck e53390ea1b Roadmap: a short user-facing ROADMAP.md, with the developer detail moved to docs/roadmap-detail.md (#6797)
* Roadmap: a short user-facing ROADMAP.md, developer detail moves to docs/roadmap-detail.md

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

* Roadmap: feature milestones with progress counts, daily releases through the in-app updater

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

* Roadmap: count issues as pieces, show work built on the drawings and render branches

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 06:49:35 +00:00
raykholo c8314687fc Construction points and axes: Browser rows, and pickable in the view and dialogs (#4312) (#4320) 2026-10-26 04:50:46 +00:00
raykholo 878339cac8 Sketch patterns: count, spacing, and angle follow parameters (#5231) (#5332) 2026-10-26 04:31:55 +00:00
bherbruck 348bbc1dfa fix(joints): edge and corner picks follow the part when it changes (#6942)
The Joint dialog kept a round edge (a hole's rim), a straight edge and a
corner as plain points where they were picked. When the part got thicker,
a hole moved or the part got longer, the origin stayed where it was and
the jointed part overlapped the other one. An edit that left the picked
spot where it was (lengthening an arm away from the jointed hole) looked
fine, which hid it. Faces and hole walls were already found by name.

The dialog now sends a round edge as its circle's centre with its z, and a
straight edge or corner as an edge pick with its z. The engine keeps them
by name (the round end by its wall and the face across its rim, the edge
and corner by their edge and face names), with the frame the pick showed;
where it can't (no planar face beside the edge, no names), it keeps the
point as before. Edge origins get an optional axis (along the edge, or a
fixed direction) so a hinge on an edge keeps turning about it.

Tests: every joint type and as-built, for each way the dialog picks an
origin, with the arm made thicker, its hole moved and the arm lengthened,
inside one design and through Update Links.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:05:26 +00:00
dudedagger42 262f5c61c0 Move, Align and previews on placed components (#4455)
* Move and Align on placed components: picks and vectors where they are shown

A body in a component placed away from its own frame (an imported part
set upright) moved the wrong way: Move's vector, axis and pivot were
taken in the root's frame, so moving the upright plate up moved it along
its own frame's z. They now turn with the frame of the bodies moved.

Align mapped each pick into the frame of the body under it, so aligning
two separately placed parts mixed two frames (the plate landed at right
angles), and its snap points were never mapped at all. Align now finds
its picks, face normals and snaps where the view shows them and takes
both ends into the moved bodies' frame.

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

* Preview: a feature's tool is drawn where its component is shown

Extrude, revolve and the primitives preview their tool on its own. That
preview was built in the model's frame, so on a component placed away
from it (an imported part set upright, edited with its component
active) the tool showed at right angles to the picked face, though the
feature itself came out right.

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

* Follow main's lazy body frames and first-part grounding in the placed-component work

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-26 03:28:14 +00:00
Luke Jones ece2a8693a Cache assembly browser data between unchanged frames (#662) (#1610)
* Cache assembly browser data between unchanged frames

Extend #1762's cached component membership, placements and viewport hover
feedback with cached occurrence labels, folder entries, row keys and
tooltips. Skip offscreen row work and reuse browser hover buffers while
reading selection, visibility and locking live. Cache #2681's standard-part
configuration labels with the document snapshot instead of formatting
them on every redraw. Preserve #3009's live selection and hover marks
with highlight keys owned once by cached entries, and retain #2431's
feature rows in the reusable drawing order.

Related to:
- tangency-labs/solidcraft#662
- tangency-labs/solidcraft#1762
- tangency-labs/solidcraft#2681
- tangency-labs/solidcraft#3009
- tangency-labs/solidcraft#2431

Co-authored-by: Codex <noreply@openai.com>

* Browser cache: lock state moved to the session visibility

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

* Browser cache: reveal a placement's body by its row, and drop a needless borrow

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

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:51:32 +00:00
dudedagger42 0f77199449 Pipe: an Operation row, so a pipe can join, cut or intersect (#2820) (#5169)
* Pipe: an Operation row, so a pipe can join, cut or intersect (#2820)

The engine's solid.pipe takes an operation, but the Pipe window had no way to
choose one, so a pipe always made a new body. The window now has the same
Operation row as Loft and the primitives, and editing a pipe keeps its
operation.

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

* Fit the Pipe operation row to main: sketch-qualified pick, length_input pattern

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-26 02:32:31 +00:00
github-actions[bot] e02ef3246f Regenerate generated files and fold pending files [skip ci] 2026-10-26 03:14:51 +00:00
raykholo ffbeb42b5c Assistant: say so when a turn ends without using SolidCraft's tools (#4846) (#5188)
* claim: #4846

* Assistant: say so when a turn ends without using SolidCraft's tools

When tools were offered and a completed turn made no tool call and no
change, the chat panel shows a short notice pointing to tool-capable
models: always for an empty reply, and for a text-only reply to an
explicit edit request. Plain answers to questions stay quiet. The
control channel exposes it as turnWarning.

* Fix the Anthropic loop test for the tools event; add ja and zh for the no-tool-call notice

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:19:57 +00:00
Matthew Knutson 816bcf10de Every new command comes with input and output schemas (#2829) (#4035)
* Every new command comes with input and output schemas

A test fails for a command without a contract or an output type, unless it is a planned!
placeholder or listed in schema/unconverted.txt, which holds the 400 commands that predate
the rule and only shrinks: the test also fails when a listed command has its schemas, when
the list names a command that doesn't exist, or when it isn't sorted.

`checked!` now takes only the command's id and checks the contract that id publishes
(`run_checked_as`), so the contract enforced is always the one described. Another test runs
every command that has a contract in strict with an unknown key, so a contract is never
published without `checked!`, under the command's own id, enforcing it.

A new book page, Command schemas, explains how to write a contract and a reply type, the
tests to add, the shared field types and how to add one, with a checklist; the failure
messages point to it. "Adding a command", CONTRIBUTING.md and AGENTS.md now include the
schemas, and the MCP tool description and docs/mcp.md no longer list the converted commands.

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

* List the commands added on main since: view.layout, view.history_layout, view.normal_to

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

* Command schemas guide: what participants and targets do, checking the evaluation, warning-free replies

- `participants` changes the result for some features and nothing for others; where it and
  another key override each other, or where it has no effect, say so in that key's
  description or in the summary.
- The example primitive's `targets` default to every body in the feature's component, not the
  bodies it meets; they're ignored when `participants` lists bodies.
- Check what each value does where the feature is evaluated, not only how the handler reads it.
- A reply test runs every form in Warn and checks for no `param_warnings` (Strict for a reply
  that isn't an object).

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

* List the 43 commands added on main since: frame, component, render and others

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

* List the 11 commands added on main since: browser folding, standard views, parameter sweep, path flange

browser.collapse_all, browser.expand_all, inspect.parameter_sweep, sheet.path_flange and the
seven view.* standard views (back, bottom, front, isometric, left, right, top) landed on main
without schemas; the gate flags them, as intended. 457 commands listed.

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

* Schema rule warns instead of failing for now

A command without input and output schemas prints a warning listing it
(test output and a CI annotation). SCHEMA_RULE_STRICT in coverage_tests.rs
(or SOLIDCRAFT_SCHEMA_RULE_STRICT=1) makes it fail again. Docs say "should,
and will become required".

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-26 01:43:02 +00:00
Nathan Dane 28689e784e fix(ui): retain macOS application bar controls alongside native menus (#6190) 2026-10-25 21:19:57 -04:00
github-actions[bot] d396be78a2 Regenerate generated files and fold pending files [skip ci] 2026-10-25 23:35:40 +00:00
github-actions[bot] cbac207852 Regenerate generated files and fold pending files [skip ci] 2026-10-25 23:29:18 +00:00
taylorhayis ff2ca2d462 Keep focus when correcting an invalid parameter name (#3965) (#4000)
* claim: #3965

* Parameters: a click can correct an invalid name (#3965)

Refusing the new row was putting focus back on the field being left, so the correction was typed into the expression.

* Parameters: the invalid-name test types its expression draft (#3965)

A new row starts with a blank expression, so the regression test types 10 mm and checks that correcting the name leaves that draft in place.
2026-10-25 22:42:30 +00:00
bherbruck 7770f8982d Release 0.23.0 2026-10-25 18:18:39 -04:00
bherbruck 8e61eb7d6f Release 0.23.0: version bump, design format 37 (#6868)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:18:36 -04:00
CamWight ab80a410a1 Viewport theme: a light 3D view in a dark window, and the reverse (#4146) (#4147)
* claim: #4146

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

* Viewport theme: a light 3D view in a dark window, and the reverse (#4146)

Preferences › Interface › Theme gets a Viewport row under the theme tiles:
Follow theme (the default, as before), Light or Dark. Light or Dark draws
the 3D view in the built-in Light or Dark theme's colours whatever the
window uses, so a dark window can show the model on a light view and the
other way round; it works with every theme, theme files included.

The view takes the background and grid, bodies, selection and hover, the
origin, previews, sketches with their dimensions and glyphs, the navigation
cube and triads, box selection and the analysis colours. Panels, dialogs,
menus and the chips over the view keep the window's. Text and warnings drawn
straight onto the view (the sketch status line, the loose-end and midpoint
markers, the section's cut outlines, the 3D sketch tool and the sketch tools'
previews) take the view's side, so they read on its background.

theme::ViewportTheme is the setting (saved as `viewport_theme`: theme, light
or dark; an unknown saved value reads as Follow theme, as ColorVision's
does). Tokens::get() takes the view's colours from the other side only when
the view's dark or light differs from the window's; Tokens::view() is the
view's side for colours drawn onto it. The ground shadow follows the view
(the scene and highlight caches already key on the colours themselves),
ui.render draws in the view's colours, and ui.prefs takes `viewport_theme`
and reports `viewport_dark`. An older release ignores the setting and draws
everything in the theme.

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

* Viewport theme: follow the single set of axis colours

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 22:17:04 +00:00
leValdemar19 b94692497e Sketch Dimension: arc length from an arc and its end point (#754) (#4245)
* claim: #754

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

* Sketch Dimension: arc length from an arc and its end point (#754)

The arc-length dimension existed (sketch.dimension type arc_length) but
the Dimension tool could only give an arc its radius. An arc and one of
its own end points, in either order, now dimension the arc's length:
click the arc, then its end. The pair was refused before ("cannot
dimension that combination"), so no existing result changes. A point
where the arc's end is (a coincident line end) counts as that end.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:17:01 +00:00
Armin 7bcc1ee0eb Drawing: shared number formatter (#5290) (#5315)
* claim: #5290

* doc: add configurable decimal number formatter (#5290)

---------

Co-authored-by: Armin <192400139+armin0411@users.noreply.github.com>
2026-10-25 22:11:39 +00:00
raykholo 36a60a70d9 Graphics: 1x anti-aliasing where the multisampled view draws nothing (#5819, #5870) (#5832) 2026-10-25 22:10:04 +00:00
dudedagger42 5d17fa970b Sketch: hovering a circle's edge wakes its centre (#5043)
* Sketch: hovering a circle's edge wakes its centre

A circle's or arc's centre was hard to take with Line or Dimension: it is a small
point in the middle of empty space, taken only within 8 points of the pointer, and
nothing showed where it was. Hovering a circle or arc of the sketch being edited
now wakes its centre: drawn (a cross in a ring) and taken from 16 points away, so
the pointer can go from the edge across the inside to it. Hovering another curve
or point puts it back to sleep, as does leaving the sketch.

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

* A woken centre's extra reach yields to a curve under the pointer

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 21:57:23 +00:00
Frank b5bab3f663 Browser: surface bodies get the surface icon in the Bodies folder (#4165) (#4168)
* Browser: surface bodies get the surface icon in the Bodies folder (#4165)

Every row in the Bodies folder had the solid body icon, so a surface (an
open shell, such as a Surface Revolve of an open curve) looked like a
solid. A body whose shell does not close now shows the existing surface
icon; solids keep the body icon.

Body::is_surface walks every shell's edges, so ModelBody caches it once per
body (shared by clones, like outline_bounds) instead of the browser
checking it every frame.

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

* Add the surface icon to the requested-icons manifest

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 21:51:02 +00:00
Steven Huff ccea34e73c sketch: a tangent whose contact is tied by Coincident, PointOnCurve or Midpoint is solved at the contact (#2584)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:37:51 +00:00
Dennis Klappe c7f86c7309 Look At in the viewport context menu for a planar face or plane (#4105) (#4497)
* Look At in the viewport context menu for a planar face or plane

Fixes #4105

* Look At tests: turn the camera without the animation

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:37:37 +00:00
raykholo 4355d117ef Assistant: warn when an Ollama model has no tool support (#4846) (#4916)
* claim: #4846

* Warn when Ollama models lack tool support (#4846)

Check Ollama model capabilities in the background and cache results per
endpoint and model. Show an advisory warning for models without tools,
expose it through ui.agent_chat, and document the behavior.

Cover capability parsing, warning selection, background probing, caching,
and selection changes with synthetic regression tests.

* Keep the Ollama capability probe out of the web build

The panel never calls it there, so it would only be dead code.

* Wrap the model warning instead of cutting it off

In a narrow panel the end of the sentence, the part that says what to do, was truncated.

* Fix AgentConfig default after rebase onto main

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

* Move the Ollama no-tools warning into a pending locale file with ja and zh

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:37:30 +00:00
dudedagger42 39951a22ff Loft: a sketch point picked in the window is an apex (#4830) (#5207)
solid.loft already closes to a point (a section {sketch, point}), but the Loft
window's Profiles input took profiles only, so a pyramid or cone tip couldn't
be made from the UI. The input now also takes a finished sketch's point, sent
as an apex section in pick order; profiles of one sketch still make one
section.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:17:14 +00:00
ChrisNo a3890b3bd5 Open a linked part from its assembly Browser row (#2348) (#2349)
* claim: #2348

* Open linked designs from assembly browser

* claim: #2348

* Test linked design action with multiple components selected

* Fix linked design lookup in browser storage and translate its menu action

* Add Turn Up Axis to complete UI catalogs

* Keep browser DXF download after linked-design checks
2026-10-25 21:15:48 +00:00
Vadim Kalinsky 59cad5f0bb Colored components (feature request #2507) (#2877)
* feat: add colored components inspection toggle

* feat: persist component colors and add cycling controls

* feat: assign component colors in creation order

* fix: update component color menu integration and translations

* fix: proper description for pending format change.

* fix: deleted duplicated format change description from docs/pending.

* Compare the component colour counter in the features-only check

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

---------

Co-authored-by: Vadim Kalinsky <vadim@kalinsky.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:11:03 +00:00
Kai Evans 5fbe058051 Sketch: a click picks the sketch's own curve over a projected one under it (#5257) (#5260)
* Sketch: a click picks the sketch's own curve over a projected one under it

* Select Other tests: follow origin visibility rename

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

* Own-curve pick test: click away from the lines' midpoints, which are picks of their own now

* Use the select-other test helpers main already has

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:10:10 +00:00
DuBearte 0752d8d79c Extrude: a profile drawn twice is cut once, not as two coincident tools (#5543) (#5789)
* claim: #5543

* Extrude: a profile drawn twice is cut once, not as two coincident tools (#5543)

A circle drawn twice at one place is two profiles of one region. Extruded
together they were two coincident tools: a cut through a 121-hole grid with
four such circles retried the boolean with the tool nudged (four times) and
took 25 s instead of 0.5 s.

merge_regions, which every extrude, revolve and surface feature runs on the
regions it builds from, now leaves out a region whose boundary repeats an
earlier one (within 1e-7 of its size; corners and segment middles on the
other loop both ways, holes alike). The selection itself keeps its
multiplicities, as profile_refs' tests require.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:10:02 +00:00
raykholo 0344a41dc4 Open and insert a DXF outside a sketch (#2052) (#4635) 2026-10-25 21:03:31 +00:00
DuBearte dba8108369 Timeline: a reorder evaluates the design once, from the cache it already has (#6186) (#6187)
* claim: #6186

* Timeline: a reorder evaluates the design once, from the cache it already has (#6186)

timeline.reorder checked the move on a copy of the model, then dropped it,
so setting the reordered design evaluated the moved part of the timeline
again; reorder_many (and moving features into a group) checked on an empty
model, recomputing the whole design even for a move near the end. The
check now starts from the session's own model, and the session keeps the
model it was checked on.

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

* Marker is Copy: drop the clone

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 20:55:52 +00:00
bherbruck 57d787f5ba Docs site: canonical links, robots.txt and sitemap.xml (#6888)
Search Console lists docs.solidcraft.io/, /introduction.html (the same page) and /guide/mcp.html as duplicates without a user-selected canonical. The Pages build now adds a canonical link to every book page (index.html and introduction.html both name /), skips the redirect stubs that already have one, marks 404 and the toc frame noindex, and writes a robots.txt pointing at a generated sitemap.xml.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:35:56 -04:00
James Barnette 2eaa15ea04 Context menu: a right-click on a face of a selected body keeps the selection (#5849) (#5915)
* claim: #5849

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

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

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

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

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

Fixes #5849.

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

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

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

---------

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 20:28:06 +00:00
github-actions[bot] 62d55c3b29 Regenerate generated files and fold pending files [skip ci] 2026-10-25 21:31:35 +00:00
NOWN-DEV 826244bb28 Text parameters: a sketch text's words built from them follow every change (#727) (#6364)
* Test: a text's words follow its text parameters, and so does its cut

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 20:13:46 +00:00
HiroKenEz b1d4293cf0 One set of X, Y and Z colours for the origin axes, view triad and Move gizmo (#976) (#4430)
* claim: #976

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

* i18n(uk): translate 114 pending messages

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

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

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

* Sort the Ukrainian catalogue

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

---------

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 19:05:48 +00:00
shop-3d 16bb97f065 I18n danish cad help 20261021 (#4223)
* i18n(da): complete upstream catalog and align CAD help terminology

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

* Sort the Danish catalogue

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

---------

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

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

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

* fix: bound recovery metadata reads and validate entry identity

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

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

* Format recovery.rs

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

---------

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

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

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

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

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

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

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

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

---------

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

* Add Norwegian Bokmål translation

* Refresh Norwegian Bokmål translations against current main

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

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

---------

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

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

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

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

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

* Drop needless to_string calls in the plane name format

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-25 18:42:24 +00:00
Kai Evans 0870d6474a Themes: Okibi (dark) and Akari (light), from the Okibi design system (#5730) (#5731)
* Themes: Okibi (dark) and Akari (light), from the Okibi design system

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

* Give the Okibi and Akari themes an icon palette

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

---------

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

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

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

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

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

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

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

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

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

* Format

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

* Format

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

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

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

* Path Flange picks keep their sketch too

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

---------

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

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

* Format the gallery sample row

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

---------

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

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

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

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

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

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

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

This reverts commit 3bc73c5930.

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

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

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

* Agent skill for reproducible bug reports (#3983)

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

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

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

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

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

* Trim the theme row scroll arrow tests

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

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

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

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

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

Tests: the_timeline_shows_only_the_active_components_features,
a_slot_among_the_shown_features_is_a_design_position.

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

* feat: distinguish cosmetic threads in shaded views

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

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

Fixes #6252.

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

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

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

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

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

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

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

Fixes #4167

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:57:29 +00:00
537 changed files with 42244 additions and 4778 deletions
+3 -1
View File
@@ -1,5 +1,6 @@
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml).
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml),
# except the GPU path tracer's tests (the gpu-path group below), which run in four shards of their own.
[profile.ci]
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
@@ -72,6 +73,7 @@ priority = 100
# Each nextest test has its own process/device. Concurrent cold lavapipe path
# compiles contend for CPU and can exhaust the 600 s pipeline setup budget.
# Serialize this module per CI shard; other tests retain their normal parallelism.
# CI runs these in their own job (GPU path tests in ci.yml), split into count partitions.
[test-groups.gpu-path]
max-threads = 1
+2
View File
@@ -6,6 +6,7 @@ body:
attributes:
value: |
Thanks for taking the time to report this! Inside the app, **Report an Issue** fills most of this in for you, with the command, its settings and your environment.
Before reporting a boolean or modelling failure, please try to reproduce it on the latest nightly or a build from current `main`, and say which build you used.
- type: textarea
id: what
attributes:
@@ -19,6 +20,7 @@ body:
label: Steps to reproduce
description: |
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
placeholder: |
1. Sketch a circle on the XY plane
2. Extrude it 10 mm
+158 -22
View File
@@ -9,12 +9,14 @@
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
# no native job runs for them, on pull requests or on main.
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
# and app startup), the reference tests and the doc tests run on main and nightly only, not on
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
# only main commits.
# checks (no archive to upload and download). The GPU path tracer's tests run one at a time
# (.config/nextest.toml), so they have a job of their own in four shards (GPU path tests). A few
# heavy tests (see .config/nextest.toml) are skipped on pull requests and run on pushes to main and
# nightly. The reference tests and the doc tests run in the merge queue, on main and nightly, not
# on pull requests (the fast lane, in Select checks); a break there is fixed forward on main. The
# Windows jobs (D3D12 shaders and app startup) run nightly, and in the merge queue only for a group
# that changes what they exercise. Only main saves the build caches; pull requests restore them.
# sccache is on a Blacksmith sticky disk that only main commits.
# A push to main runs only the repository checks when a passing merge-queue run tested that very
# commit, or when nothing a build reads changed since the last push to main whose run passed.
# Otherwise it runs everything.
@@ -30,7 +32,7 @@
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
@@ -103,6 +105,9 @@ jobs:
kernel: ${{ steps.paths.outputs.kernel }}
goldens: ${{ steps.paths.outputs.goldens }}
windows: ${{ steps.paths.outputs.windows }}
# The GPU path tests run: native, and the whole workspace or solidcraft-gpu selected. The
# job's `if` and the `ci` job's check both read this, so they cannot disagree.
gpu: ${{ steps.paths.outputs.gpu }}
full: ${{ steps.paths.outputs.full }}
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
quick: ${{ steps.paths.outputs.quick }}
@@ -122,6 +127,8 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_SHA: ${{ github.sha }}
GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
@@ -144,6 +151,23 @@ jobs:
full=$(sed -n 's/^full=//p' "$RUNNER_TEMP/ci-affected")
packages=$(sed -n 's/^packages=//p' "$RUNNER_TEMP/ci-affected")
}
# Whether the NUL-separated paths in $1 (the diff $2) reach what the Windows D3D12 jobs
# exercise: the GPU crate and its shaders, the app and its startup, the viewport's wgpu
# glue, the vendored egui-wgpu, their scripts, or a lockfile change that moves the
# graphics stack (wgpu, naga, egui, winit, windows). Pull requests and merge groups share it.
windows_paths() {
while IFS= read -r -d '' path; do
case "$path" in
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
esac
done < "$1"
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$1" \
&& git diff "$2" -- Cargo.lock \
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
windows=true
fi
}
if [ "$EVENT_NAME" = push ]; then
# The queue lands the very commit its merge_group run tested. If that run passed,
# testing the push again proves nothing new; otherwise (a merge that skipped the
@@ -164,13 +188,25 @@ jobs:
# push before may never have run: a newer push cancels a waiting one).
echo "$tested..$PUSH_SHA changes no code: repository checks only."
else
# Code changed (or the push can't be compared): main runs everything.
# Code changed (or the push can't be compared): main runs everything but Windows.
# The merge queue ran Windows for the changes that reach it, and the nightly runs it
# for everything else (it passed 129 of 129 non-PR runs before this).
full=true
packages=
native=true
fi
elif [ "$EVENT_NAME" = merge_group ]; then
native=true
# Windows (about 16 runner-minutes) only for a group that changes what it exercises;
# the nightly runs it whatever changed. A group that can't be compared runs it.
if git diff --no-renames --name-only -z "$GROUP_BASE_SHA...$GROUP_HEAD_SHA" > "$RUNNER_TEMP/ci-paths"; then
windows_paths "$RUNNER_TEMP/ci-paths" "$GROUP_BASE_SHA...$GROUP_HEAD_SHA"
else
windows=true
fi
echo "windows=$windows for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
elif [ "$EVENT_NAME" != pull_request ]; then
# The nightly run and Run workflow: everything, Windows included.
native=true
windows=true
else
@@ -185,19 +221,9 @@ jobs:
case "$path" in
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
esac
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
case "$path" in
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
esac
done < "$RUNNER_TEMP/ci-paths"
# A lockfile change that moves the graphics stack (wgpu, naga, egui, winit, windows).
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$RUNNER_TEMP/ci-paths" \
&& git diff "$BASE_SHA...$HEAD_SHA" -- Cargo.lock \
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
windows=true
fi
# Computed as for a merge group, then turned off below (the fast lane).
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
# Pull requests build and test the touched packages and their dependents; nothing
# selected means no code changed.
select_packages "$RUNNER_TEMP/ci-paths"
@@ -242,8 +268,16 @@ jobs:
goldens=false
fi
fi
# The GPU path tests (their own job) run with the other tests, when solidcraft-gpu is
# among the packages they test.
gpu=false
if [ "$native" = true ]; then
case " $packages " in *" solidcraft-gpu "*) gpu=true ;; esac
if [ "$full" = true ]; then gpu=true; fi
fi
{
echo "native=$native"
echo "gpu=$gpu"
echo "kernel=$kernel"
echo "goldens=$goldens"
echo "windows=$windows"
@@ -286,6 +320,10 @@ jobs:
cargo test -p xtask ui_containers --locked
- name: Sorted lists
run: cargo xtask lists
- name: Agent router and skills cite real files
run: |
cargo xtask skills
cargo test -p xtask skills --locked
- name: Design format pending changes
run: bash scripts/test-format-pending.sh
- name: Web app offline use and updates (headless Chrome)
@@ -487,8 +525,10 @@ jobs:
scope=(--workspace)
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
status=0
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
# took about 36 of this job's 45 minutes.
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E "$base" || status=1
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=1
if [ "$status" != 0 ]; then
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
@@ -508,6 +548,96 @@ jobs:
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
retention-days: 7
gpu-path-tests:
name: GPU path tests (${{ matrix.shard }}/4)
# The GPU path tracer's tests (about 108) compile their pipelines on lavapipe, one test at a
# time (the gpu-path test group in .config/nextest.toml keeps them within the 600 s pipeline
# setup budget). Serial, they took about 36 minutes, the longest chain in Native Linux tests;
# here four shards run beside it. Their filter is the one Native Linux tests leaves out, so
# every test runs exactly once.
# 16 vCPU, not 8: lavapipe compiles on every core, and on 8 vCPU one test went past the 600 s
# budget (665 s, against 204 s for the same test in another run) and a shard took 35 minutes.
needs: changes
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.gpu == 'true' }}
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
# As in Native Linux tests: lavapipe draws, and a test without an adapter fails.
SOLIDCRAFT_GPU: software
SOLIDCRAFT_REQUIRE_GPU: "1"
# lavapipe's own shader cache (below).
MESA_SHADER_CACHE_DIR: /home/runner/.cache/mesa-gpu
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /home/runner/.cache/sccache
SCCACHE_CACHE_SIZE: 20G
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install a software Vulkan driver (lavapipe)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1
- name: Size parallel compiles to the job's memory
run: scripts/ci/build-jobs.sh
- uses: dtolnay/rust-toolchain@stable
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
# forks mount a clone and never write back, so nothing they build can reach main's cache.
# The shards build the same thing, so only the first one commits it (and saves the cache).
- name: Mount the compiler cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-sccache-gpu
path: /home/runner/.cache/sccache
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
# The first test in a shard to build the path pipeline compiles it cold on lavapipe (335 to
# 665 s, against the 600 s setup budget); every later one reads Mesa's disk cache (about
# 30 s). Keeping that cache from main's last run lets the first test read it too, until the
# shaders change. Same sticky disk rules as the compiler cache.
- name: Mount lavapipe's shader cache
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
continue-on-error: true
with:
key: ${{ github.repository }}-mesa-gpu
path: /home/runner/.cache/mesa-gpu
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
# Main saves the cache; every other run restores it, read-only.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: native-gpu
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
- name: Set up compiler cache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
tool: cargo-nextest
- name: Run one shard of the GPU path tests
env:
SHARD: ${{ matrix.shard }}
run: |
# None of these is in the default filter (the heavy tests), so pull requests, the merge
# queue, main and nightly all run every one of them, as before. `count` deals the tests
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4"
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
- name: Upload test timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-gpu-${{ matrix.shard }}
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
retention-days: 7
action-matrix:
# Every ordinary action on every face, edge, vertex and body of the design corpus, against
# tests/action_matrix/baseline.json (docs/action-matrix.md). Every test run checks a sample;
@@ -769,7 +899,7 @@ jobs:
# commit's real run instead: the merge queue reads `ci` from the newest run of a commit, so a
# run without a passing `ci` would block a pull request that passed (#1850, #3182, #3252).
name: ci
needs: [changes, checks, quick-compile, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
@@ -817,6 +947,8 @@ jobs:
BUILD: ${{ needs.build-tests.result }}
EVENT_NAME: ${{ github.event_name }}
TESTS: ${{ needs.tests.result }}
GPU_RUN: ${{ needs.changes.outputs.gpu }}
GPU: ${{ needs.gpu-path-tests.result }}
DOCS: ${{ needs.doc-tests.result }}
ORACLE: ${{ needs.oracle.result }}
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
@@ -836,6 +968,10 @@ jobs:
fi
test "$TESTS" = success
fi
# The same output decides whether the GPU path tests run (their job's `if`).
if [ "$GPU_RUN" = true ]; then
test "$GPU" = success
fi
if [ "$DOCTESTS" = true ]; then
test "$DOCS" = success
fi
+42
View File
@@ -0,0 +1,42 @@
# Nightly import checks on real-world STEP files (#1397): the external corpus in
# tests/external-corpus.toml is downloaded (hash-verified), imported headless, and checked
# against tolerance-based expectations — body counts, total volume and area, time and memory
# budgets. It is not part of `cargo xtask ci`: it downloads from the internet.
#
# Import results should improve over time (healing, edge merging, meshing). When a check fails
# because the numbers moved for the better (or the source changed upstream), re-record with
# `cargo xtask external-corpus --bless "why"` in a pull request, never by editing numbers.
name: External corpus
on:
schedule:
- cron: '40 3 * * *'
workflow_dispatch:
inputs:
only:
description: "Run only this corpus entry name (empty = every entry)"
type: string
default: ""
permissions:
contents: read
jobs:
corpus:
name: Import the external STEP corpus
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- name: Build the CLI
run: cargo build --profile ci -p solidcraft-cli
- name: Import and check every entry
env:
ONLY: ${{ inputs.only }}
run: |
if [ -z "$ONLY" ]; then
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli
else
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli --only "$ONLY"
fi
+1 -1
View File
@@ -16,7 +16,7 @@ on:
permissions:
contents: read # to check out scripts/kernel-paused
issues: write
pull-requests: read # to list a PR's changed paths
pull-requests: write # to list a PR's changed paths and add its label
discussions: write
concurrency:
+1
View File
@@ -65,6 +65,7 @@ jobs:
run: |
mkdir -p site
cp -r target/book/. site/
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
cp -r dist/web site/app
- uses: actions/configure-pages@v5
id: pages
+125 -1
View File
@@ -5,7 +5,9 @@
# the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases.
# At midnight Eastern, changed main commits build and publish a nightly pre-release.
# A version with a suffix (0.2.0-rc.1) is a pre-release.
# The macOS app is signed and notarized when the APPLE_* secrets exist, nothing else is signed yet.
# The macOS app is signed and notarized when the APPLE_* secrets exist. The Windows binaries and
# MSI are Authenticode-signed through SignPath Foundation when the SIGNPATH_API_TOKEN secret exists
# (see the windows job). Nothing else is signed yet.
#
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
@@ -18,6 +20,8 @@
# releases/latest/download/<name> links in the README keep working across releases.
#
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
name: Release
on:
@@ -32,6 +36,10 @@ on:
description: "Create/update a draft (release branch only; off: build artifacts only)"
type: boolean
default: false
signpath_signing_policy:
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
type: string
default: ""
permissions:
contents: read
@@ -378,7 +386,14 @@ jobs:
name: Windows ${{ matrix.arch }}
needs: [version, icons]
if: needs.version.outputs.build == 'true'
# Must stay a GitHub-hosted runner, like every job before it: SignPath's open-source policy
# only signs builds whose jobs all ran on GitHub-hosted runners.
runs-on: windows-latest
# actions: read lets the SignPath action read this job's details and fetch the uploaded
# artifact with GITHUB_TOKEN (the workflow default above grants contents: read only).
permissions:
contents: read
actions: read
strategy:
fail-fast: false
matrix:
@@ -389,6 +404,16 @@ jobs:
- { arch: arm64, target: aarch64-pc-windows-msvc }
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
# Authenticode signing through SignPath Foundation (docs/releasing.md, "Windows signing").
# Only when the API token secret exists: forks and repositories without it build unsigned,
# exactly as before. `if:` can't read secrets, so the check goes through this variable.
SIGN_WINDOWS: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
# The one switch between the self-signed test certificate (test-signing) and the real one
# (release-signing): a dispatch input, else the SIGNPATH_SIGNING_POLICY repository variable,
# else test-signing.
SIGNPATH_SIGNING_POLICY: ${{ inputs.signpath_signing_policy || vars.SIGNPATH_SIGNING_POLICY || 'test-signing' }}
SIGNPATH_ORGANIZATION_ID: fa603ff4-c702-404b-8588-a524371c7a4e
SIGNPATH_PROJECT_SLUG: solidcraft
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
@@ -407,10 +432,109 @@ jobs:
dotnet tool install --global wix --version 5.0.2
wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2
- name: Build and package (msi, portable zip)
if: env.SIGN_WINDOWS != 'true'
shell: pwsh
env:
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
# ---- signed path ---------------------------------------------------------------------------
# SignPath's GitHub integration (docs.signpath.io/trusted-build-systems/github): the files go
# up as a workflow artifact, SignPath fetches that artifact itself (so it can verify it came
# from this workflow run), signs it, and the action downloads the signed copy. GitHub stores
# artifacts as zips, so packaging/windows/signpath-artifact-configuration.xml (pasted into the
# SignPath project) describes a zip holding the files at its top level.
# Two rounds: the .exe files first, so the MSI, the portable zip and the plain .exe all carry
# signed binaries; then the MSI built from them.
- name: Build and stage the binaries for signing
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
env:
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase stage
- name: Upload the unsigned binaries
id: unsigned-exes
if: env.SIGN_WINDOWS == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-unsigned-exes-windows-${{ matrix.arch }}
path: target/windows-package/${{ matrix.arch }}/*.exe
if-no-files-found: error
retention-days: 3
- name: Sign the binaries (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
if: env.SIGN_WINDOWS == 'true'
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
github-artifact-id: ${{ steps.unsigned-exes.outputs.artifact-id }}
wait-for-completion: true
# release-signing requests wait for a person to approve them in SignPath; give that an
# hour instead of the default 10 minutes. test-signing requests finish in seconds.
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: target/windows-package/signed-exes-${{ matrix.arch }}
- name: Put the signed binaries in place
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$stage = "target/windows-package/${{ matrix.arch }}"
foreach ($exe in 'solidcraft.exe', 'solidcraft-cli.exe', 'solvecraft-cli.exe') {
$signed = "target/windows-package/signed-exes-${{ matrix.arch }}/$exe"
# The artifact configuration matches each file optionally, so check that every file
# actually came back signed rather than trusting the request's success.
$sig = Get-AuthenticodeSignature $signed
if (-not $sig.SignerCertificate) { throw "$exe came back from SignPath without a signature ($($sig.Status))" }
# The test certificate is self-signed, so Status is not Valid until release-signing.
Write-Output "$exe signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
Copy-Item -Force $signed "$stage/$exe"
}
- name: Package the signed binaries (msi, portable zip)
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
env:
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase package
- name: Upload the unsigned MSI
id: unsigned-msi
if: env.SIGN_WINDOWS == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-unsigned-msi-windows-${{ matrix.arch }}
path: dist/release/*.msi
if-no-files-found: error
retention-days: 3
- name: Sign the MSI (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
if: env.SIGN_WINDOWS == 'true'
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
github-artifact-id: ${{ steps.unsigned-msi.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: target/windows-package/signed-msi-${{ matrix.arch }}
- name: Put the signed MSI in place
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$msi = Get-Item "dist/release/*.msi"
$signed = "target/windows-package/signed-msi-${{ matrix.arch }}/$($msi.Name)"
$sig = Get-AuthenticodeSignature $signed
if (-not $sig.SignerCertificate) { throw "$($msi.Name) came back from SignPath without a signature ($($sig.Status))" }
Write-Output "$($msi.Name) signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
Copy-Item -Force $signed $msi.FullName
"Windows ${{ matrix.arch }}: Authenticode-signed through SignPath (policy $env:SIGNPATH_SIGNING_POLICY, $($sig.SignerCertificate.Subject))." >> $env:GITHUB_STEP_SUMMARY
- name: Note the unsigned build
if: env.SIGN_WINDOWS != 'true'
shell: pwsh
run: '"Windows ${{ matrix.arch }}: unsigned (no SIGNPATH_API_TOKEN secret)." >> $env:GITHUB_STEP_SUMMARY'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-windows-${{ matrix.arch }}
+4
View File
@@ -11,3 +11,7 @@ __pycache__/
# cargo-mutants output (docs/mutation-testing.md)
mutants.out*/
# external corpus downloads (cargo xtask external-corpus, #1397): fetched at test time,
# hash-pinned in tests/external-corpus.toml; never commit case data
/corpus/
+44 -218
View File
@@ -1,222 +1,48 @@
# SolidCraft — instructions for agents
# SolidCraft: instructions for agents
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
sketches with a constraint solver, a parametric timeline of features, a B-rep
kernel, and a desktop UI that agents can drive. Projects published by
[storytold](https://github.com/storytold) can serve as references for implementation conventions.
Any code adapted from other projects (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
## Product name
SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`,
`.solidcraft` designs, the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables,
settings and recovery folders, the MCP server name, docs, CI and packaging. The old name
survives only in the explicit compatibility shims listed in `docs/compatibility.md`; don't add
new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pages
`/solidcraft/`; do not add permanent old-name Pages aliases.
## Start here
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
## Start every session here
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
and the relevant `plan/architecture.md` section.
2. Kernel and library decisions: `plan/adr/0001-geometry-kernel.md` (what we may depend on, what
is rejected for licence reasons).
3. Follow the autonomous operation protocol (`plan/execution-plan.md` §6). Don't stop to ask
unless it lists the decision as the owner's.
## Hard rules (what enforces each)
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
- Application code is Rust only and builds for wasm: `cargo xtask wasm`, `cargo xtask web-offline-test`.
- The product name is SolidCraft; the old name lives only in `docs/compatibility.md`.
- Sorted lists (message catalogs, `mod` lines, languages) stay sorted: `cargo xtask lists`. Never edit generated files (`docs/features.md`, `THIRD-PARTY-LICENSES.txt`) in a PR.
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
- Show observable changes with real screenshots or a recording, captured yourself.
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
`plan/` is gitignored (local only).
## Never crash
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The exceptions, in the app crate and in
`crates/threedx-client` (which deny it instead), have no safe API: the macOS-only
`crates/threedx-client/src/macos.rs` (loads the 3Dconnexion driver's client framework at run time
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), 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, and gives its class the icon the taskbar falls back
on) 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
input): `get()` not `[i]`, finite checks, caps on sizes and loop counts.
- **Third-party kernel calls are guarded:** everything that calls `truck` goes through
`solidcraft_kernel::guard`, which turns a panic into `KernelError::Internal`.
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an escaped
panic restores the design and reports `EngineError::Internal`.
- **Crash reports:** the desktop app writes every panic and every unclean exit to a local crash folder,
with the design and the commands that reproduce it (`solidcraft_engine::crash`, docs/crash-reports.md).
When someone says the app crashed, look there first: `solidcraft-cli crashes`, `crashes show <id>`,
`crashes replay <id>` (or the `diagnostics.crash_reports` command / MCP `crash_reports` tool).
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`); a
crash report's `design.solidcraft` + `replay.json` is the test's starting point.
- **Where tests go:** a regression test goes into the existing test file for its area (booleans, blends,
shell, sweep, STEP, naming…), never a new file named after the bug (#3226).
- Every production crate root carries
`#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`.
## Non-negotiables
- **Independent.** SolidCraft is independent. Behaviour is designed from public documentation,
general CAD conventions and user feedback. Reference tests use analytic values and open-source
tools. Never copy another product's code, icons, artwork, help text, tooltips, templates or
materials. File formats come from public specifications.
- **No commercial CAD products.** Agents must not drive, script, automate or extract data from
commercial CAD products (Fusion, SolidWorks, Onshape, …) for this project: no test data,
measurements, command lists, ids or files produced with them.
- **Never read, copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs),
SolveSpace, CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. (`tools/freecad-oracle/`
runs FreeCAD as a separate program through its public scripting API to compute reference values;
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
operations are our own.
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
artwork from commercial CAD products — ever.** Command icons are the original SVG masters in
`assets/icons/ui/design/svg/`, compiled through `crates/ui-egui/src/icon_svg.rs`.
**Before any icon, icon assignment or media edit, read `assets/AGENTS.md`,
`assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`.** Use the canonical
bases and named finished references there; never invent a parallel icon style. Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
(`CommandSpec`: id = our own dotted, lower-case id `<area>.<thing>[.<variant>]` such as
`solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
clicks (`ui.start`) start interactive tools and dialogs, which then run commands.
- **Layering** is enforced by `cargo xtask layers`: L0 `geom` → L1 `sketch`, `kernel`, `render`
→ L2 `doc` → L3 `io` → L4 `engine` → L5 `ui-egui` → apps. Nothing below L5 depends on
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **One outer UI container**: all panels, floating sheets, menus, overlays and platform UI
windows use `ui_kit::Container` (directly or through `Panel`, `Window`, `Confirm`, `Menu`).
Declare the semantic `Surface`; workspace panels use its shared layout policy and chrome.
System/report windows use `ui_kit::Window`; its low-level host builder is kit-private.
Do not construct raw egui hosts, outer popup/window frames or shadows in feature modules.
`cargo xtask ui-containers` recursively checks source/examples in all current and future crates
and apps. It runs in the fast repository CI job and `cargo xtask ci`, with no caller allowances.
See `docs/ui-container-audit.md` for the inventory and legitimate content/native exceptions.
- **One set of camera controls**: every page or canvas with a camera (viewport, Render
preview, future flat pattern or drawing pages) calls `navigation::handle` with its `NavCaps`
(orbit off for flat 2D pages). New canvases must use `navigation::handle`; never hand-roll
camera input (a test rejects it).
- **Application code is Rust only.** Units are millimetres and radians internally; Z is up.
Local asset authoring, lint and review tools use Python 3.11+ as documented in
`assets/AGENTS.md`; Python is not an application runtime dependency. The application exception is
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
assets, layers). Commit after every feature arc that builds.
- **Publishing**: the repository is github.com/tangency-labs/solidcraft. Changes land through
**pull requests** and the **merge queue**: push a branch and open a PR. The heavy CI jobs start
when a maintainer adds `approved-for-ci` after reviewing the code. Approved PRs go into the queue
with `gh pr merge N -R tangency-labs/solidcraft --squash --auto`; the queue runs the full suite
on each group before it lands on `main`. Keep each PR to one topic and one issue, and never
force-push `main`. Never commit `plan/`, build dirs (`target*/`) or generated reference data;
check `git ls-files` before pushing.
- **Never break wasm**: everything below the apps must build for `wasm32-unknown-unknown`
(`cargo xtask wasm`, part of `cargo xtask ci`).
## Showing and following through on changes
- Help reviewers see what is broken or changed. For observable CAD, UI or model behavior,
issues and PRs must include actual descriptive screenshots or a short recording from a
small reproduction. This includes kernel, geometry, assembly and backend changes whose
effects can be shown in the app. Coding agents should run the app and capture this evidence
themselves; do not ask a human for evidence the agent can produce. Show the actual failure
or result, preferably before and after, with brief reproduction steps, expected/actual
behavior and annotations that point out the relevant detail.
- Flowcharts and Mermaid diagrams cannot substitute for screenshots or recordings of visible
behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate
the point. Changes with no useful observable behavior do not need screenshot or video evidence.
- Use a small public-safe example and check for private designs, paths, account details or
other sensitive content before sharing. Attach media to the issue/PR or link to an accessible
recording; do not commit generated screenshots or videos just to provide review evidence.
- Throughout the work and before handoff, check the relevant PR's merge status. Don't push to
an open PR only to catch up with `main` (no merge of `main`, no Update branch, no rebase for
that alone): every push restarts the whole CI run, and `main`'s own run catches what lands in
between, to be fixed forward. When you push a change anyway, or GitHub reports a conflict,
rebase onto the latest `main` (push your own branch with `--force-with-lease`); never merge
`main` into a PR. Resolve conflicts without discarding others' work; never force-push `main`.
Don't re-run CI on an unchanged commit: a real failure fails again. Re-run only a job that
runner trouble cancelled, with "Re-run failed jobs". Report the PR state you checked and any
conflicts resolved. If a PR remains open, keep monitoring it while the task is active and
make its pending status clear at handoff; do not claim it merged without checking.
## Before you start: look at what is already open
- Work with an authenticated GitHub CLI (`gh auth status`) and `git`; the checks below need both.
- Pull the whole list, not one search: `gh pr list -R tangency-labs/solidcraft --state open --limit 500`
(drafts included) and `gh issue list -R tangency-labs/solidcraft --state open --limit 500`. Look for
work on the same issue, the same feature under another number, and PRs that change the files
you are about to change (`gh pr list ... --json number,title,files`).
- If someone already has it, comment on their PR or issue instead of opening a second one. Two
PRs for one change can't both land, and one of them is wasted work and CI.
- Keep the `Changes a default:` line in the PR description filled in, and don't open a second PR for
an issue a lower-numbered open PR already closes; `pr-checks.yml` fails both (CONTRIBUTING.md, "Sending a change").
## Claim = a draft PR
- Before working on issue #N, check nobody has it: `gh pr list -R tangency-labs/solidcraft --state open --search "#N"`.
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
and open a **draft** PR titled `<summary> (#N)` right away. **The lowest-numbered open PR for an
issue wins**; if yours lost, close it. Opening the PR is the claim: the bot assigns you and
labels the issue `in progress` (or says on your PR who holds it). Reference `#N` in the PR title
so the bot can find it. Don't claim by editing files; comment `/claim` on the issue only to stake
a claim before you have a branch.
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
comment on it first, then open your own claim (you may start from its branch).
This takeover policy is separate from the bot's automatic cleanup deadlines in CONTRIBUTING.md.
- On a shared build machine, check the load before a big build (`cut -d' ' -f1 /proc/loadavg`)
and wait while it is over 24; build with `nice`.
## Design file format
A change that saves something new in a design adds ONE file, `crates/doc/src/format/pending/<name>.rs`
(a `//!` note, and `pub fn step` if older designs must be rewritten). Never edit `FORMAT`,
`JSON_STEPS`, the history in `format.rs`, another change's file or a `v<N>/` folder: the build
works out the number (the next one while anything is pending), and the release folds pending
changes into `v<N>/` with `scripts/fold-format-pending.sh`. See CONTRIBUTING.md.
## Application releases
Development pushes are not application releases. Follow `docs/releasing.md` when asked to
prepare or ship a release: choose the version from the actual changes, update Cargo.toml and
Cargo.lock, pass the quality gates, advance `release` without force-pushing, and verify the
resulting draft and its artifacts. When shipping is authorized, publish that verified draft
and confirm its tag and downstream deployment. Do not auto-release every push to `main`, infer
version bumps solely from commit prefixes, overwrite published versions, or push release tags
before the draft is verified.
## Running and looking at the app
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
request on a connection carries the launch's token as `"token"`: read it from
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
- **For UI work, look at the result** (screenshot, read the PNG). Without a window use
`ui.render` or `solidcraft-cli snapshot`.
- Headless: `solidcraft-cli run script.json --out part.step`, `solidcraft-cli eval design.solidcraft`,
`solidcraft-cli snapshot script.json --out shot.png`, `solidcraft-cli commands`.
- Reference tests: `cargo xtask oracle` replays `tests/oracle/*/recipe.json` and compares with
each case's `expected.json` (analytic or FreeCAD values; writes `docs/oracle.md`). Expected
values never come from a commercial CAD product.
- Build with `CARGO_TARGET_DIR=<repo>/target`. Windows: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`.
## Roadmap
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a
milestone task lands. `docs/features.md` (SolidCraft's commands by area, from the registry) and
`THIRD-PARTY-LICENSES.txt` are regenerated on `main` after each merge by the Generated files
workflow; never regenerate them in a PR.
## Lists many PRs add to
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
then by id, generated files left to `main`, and a design-format change as one new file.
New UI strings go in a new `assets/locales/pending/<slug>.json` and new ATTRIBUTION.md rows in a new
`attribution/pending/<slug>.md`, never into the catalogs or the table (see the README in each folder).
## Working on X: read the skill first
| Working on | Read |
|---|---|
| A PR, a claim, review, CI or evidence | `skills/solidcraft-contributing/SKILL.md` |
| Licences, assets, clean-room, the name | `skills/solidcraft-ground-rules/SKILL.md` |
| Non-test Rust, hostile input, a crash report | `skills/solidcraft-crash-safety/SKILL.md` |
| A command, a crate boundary | `skills/solidcraft-commands/SKILL.md` |
| A dialog, panel, camera, icon, colour | `skills/solidcraft-ui/SKILL.md` |
| Labels, messages, translations | `skills/solidcraft-ui-text/SKILL.md` |
| Saving something new in a design, import, export | `skills/solidcraft-file-formats/SKILL.md` |
| Preferences, a changed default | `skills/solidcraft-preferences/SKILL.md` |
| Writing or running tests | `skills/solidcraft-tests/SKILL.md` |
| Slow work, the window thread, wasm | `skills/solidcraft-performance/SKILL.md` |
| Releases, installers, packages | `skills/solidcraft-packaging/SKILL.md` |
| Running the app, control channel, MCP tools | `skills/solidcraft-agent-tools/SKILL.md` |
| Building a part over MCP | `skills/solidcraft-modeling/SKILL.md` |
| Changing an existing design | `skills/solidcraft-editing/SKILL.md` |
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
+21 -8
View File
@@ -51,10 +51,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
@@ -62,10 +62,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
@@ -158,6 +158,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
| assets/locales/nb.json | SolidCraft contributors | Original Norwegian Bokmål translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
@@ -173,6 +174,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/settings.py | SolidCraft contributors | dmgbuild settings (MIT, https://github.com/dmgbuild/dmgbuild) for the SolidCraft disk image; original settings, no dmgbuild code copied | MIT OR Apache-2.0 |
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
@@ -199,6 +204,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
@@ -281,6 +288,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/circle_tt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -514,6 +523,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
| docs/sponsors/blacksmith-ci-powered.png | Blacksmith (blacksmith.sh) | "CI powered by Blacksmith" sponsor banner supplied by Blacksmith for the sponsorship credit; used only to credit the sponsor, not part of the application | © Blacksmith, used with permission |
| assets/icons/ui/design/svg/model/path_flange.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| docs/book/src/generated/hem.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/hem.json`; gitignored), original sheet metal demonstration | MIT OR Apache-2.0 |
| docs/book/scripts/hem.json | SolidCraft contributors | original parametric recipe for an 80 × 50 mm sheet with an 8 mm returned edge, authored for the command guide | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay.json | SolidCraft contributors | original SolidCraft theme file (Clay, light): warm paper palette chosen for SolidCraft | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay-dark.json | SolidCraft contributors | original SolidCraft theme file (Clay Dark): warm charcoal palette chosen for SolidCraft | MIT OR Apache-2.0 |
## Code adapted from sibling projects
+26 -7
View File
@@ -1,8 +1,10 @@
# Contributing to SolidCraft
Agents: start from the router in [AGENTS.md](AGENTS.md), which points to a short skill in [`skills/`](skills/README.md) for each kind of work. This page stays the full human guide.
SolidCraft is an open-source parametric 3D CAD app written in pure Rust.
You sketch on a plane, constrain the sketch, turn profiles into solids, and
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, macOS, Linux), in
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
model with.
@@ -11,7 +13,7 @@ build it, how the code is laid out, the few rules that aren't negotiable, and wh
needed most.
Before choosing a task, check the [command inventory](docs/features.md) and
[roadmap](ROADMAP.md) for implemented capabilities and current priorities.
[roadmap](docs/roadmap-detail.md) for implemented capabilities and current priorities.
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
@@ -130,7 +132,9 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
- Every call into truck goes through `solidcraft_kernel::guard`.
- Every crash fix comes with a regression test.
5. **Everything is a command, with a test.** A new feature means a `CommandSpec` in
`crates/engine/src/cmd/`, engine tests that measure the result (volume, area,
`crates/engine/src/cmd/` with input and output schemas (see
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
command without them, and will fail it once required), engine tests that measure the result (volume, area,
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
@@ -190,9 +194,11 @@ in the PR. `cargo xtask test` builds the tests with it and then runs every test
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
worker consumes one of those slots. Every binary and side check still runs after failures.
Without the variable the existing concurrency is unchanged. `RUST_TEST_THREADS`,
`ACTION_MATRIX_JOBS` and Cargo's build-job setting remain independent caller overrides;
this limit does not choose their values or change test filters, timeouts or baselines.
Without the variable the existing concurrency is unchanged. With it set, each test binary
inherits `RUST_TEST_THREADS` and `ACTION_MATRIX_JOBS` of `max(1, cpus / jobs)` — the machine's
parallelism shared between the binaries in flight — unless you set either one explicitly;
explicit caller overrides always win. Cargo's build-job setting remains an independent caller
override; this limit does not change test filters, timeouts or baselines.
Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
@@ -274,6 +280,9 @@ and a fresh CI run:
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
- **`mod` lines**: one per line, in the sorted group they belong to (`cargo fmt` keeps each group
sorted). Add a new module into the existing group, not as a new group at the end.
- **Commands without schemas** (`crates/engine/src/schema/unconverted.txt`): sorted, one id per
line, and it only shrinks. Converting a command removes its line; a new command is never added
to it.
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
checks it).
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
@@ -334,6 +343,16 @@ Changes with no useful observable behavior do not need screenshot or video evide
small example without private designs or sensitive details; attach the media or link to an
accessible recording rather than committing generated review artifacts.
To show a picture without committing it, attach the media to the PR (it goes to the PR's own
attachment storage and renders inline in the comment). A gist is the better home for a log or
another text file, and you link it; a gist does not render a picture inline. The review artifacts
stay out of the diff that way, and nothing has to be pushed to a branch of its own.
Attaching works in the browser for anyone who can comment: drag the file into a comment on the
pull request. A command-line agent without a browser session usually cannot attach a file (the
upload needs write access to this repository). Say so in the pull request, and the person running
the agent or a maintainer can attach it.
Keep an eye on your PR's status, but don't push to it only to catch up with `main` (no merge of
`main`, no Update branch button, no rebase for that alone): every push restarts the whole CI
run for everyone waiting behind it, and `main`'s own run catches what lands in between. When you
@@ -389,7 +408,7 @@ In order of how much they'd help users:
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
through without a kernel error. Each failing seed is a reproducible bug.
3. **Drawings, CAM and mesh tools.** These are out of scope for now (see
[ROADMAP.md](ROADMAP.md)), but anyone who wants to own one is welcome.
[ROADMAP.md](docs/roadmap-detail.md)), but anyone who wants to own one is welcome.
## Updating the generated feature list
Generated
+22 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4240,11 +4240,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.22.1"
version = "0.24.0"
[[package]]
name = "solidcraft-cli"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"image",
"log",
@@ -4256,7 +4256,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"proc-macro2",
"quote",
@@ -4265,7 +4265,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"log",
"serde",
@@ -4282,7 +4282,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"log",
"schemars",
@@ -4302,7 +4302,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"serde",
"serde_json",
@@ -4311,7 +4311,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"image",
"log",
@@ -4331,7 +4331,7 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
@@ -4355,7 +4355,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"log",
"proptest",
@@ -4375,7 +4375,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4384,7 +4384,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"image",
"moxcms",
@@ -4398,7 +4398,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"serde",
"serde_json",
@@ -4409,14 +4409,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"arboard",
"egui",
@@ -4445,15 +4445,17 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"eframe",
"egui",
"js-sys",
"log",
"percent-encoding",
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -5982,12 +5984,15 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.22.1"
version = "0.24.0"
dependencies = [
"proc-macro2",
"serde",
"serde_json",
"sha2 0.10.9",
"syn 2.0.119",
"toml",
"zip",
]
[[package]]
+1 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.22.1"
version = "0.24.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+7 -5
View File
@@ -46,13 +46,13 @@
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
</p>
![SolidCraft 0.8: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo-0.8.gif)
![SolidCraft: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo.gif)
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
- **Windows:** the [installer][windows-msi], or `winget install bherbruck.SolidCraft`.
- **Windows:** the [installer][windows-msi].
- **macOS:** the [disk image][macos-dmg] (Apple silicon and Intel).
- **Linux:** the [AppImage][linux-appimage], or `sudo snap install solidcraft` ([Snap Store][snap-store]).
- **Web:** [try it in your browser][web-app], no install.
@@ -96,8 +96,10 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
`solidcraft-cli mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Headless it
runs its own design session, or with `--connect` it drives the running app so you can watch.
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Tools, access rules and an
example session: [docs/mcp.md](docs/mcp.md).
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
[docs/mcp.md](docs/mcp.md).
## Scripts
+80 -694
View File
@@ -1,696 +1,82 @@
# SolidCraft roadmap
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
## At a glance
| Question | Answer |
|---|---|
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
## Scope (owner decision, 2026-10-07)
The issue batch adds preservation of blind pockets in through cuts, associative projected
offsets and plastic face placements, XZ (+Y normal) with a v6 migration preserving older
designs, interactive angle placement, file-picker sketch imports, Home/recent controls,
toolbar overflow, and the remaining component-frame and assembly WIP work. The enclosure
acceptance scenarios now include board resizes; STEP reopening is automated. See [WIP reconciliation](docs/wip-reconciliation.md).
Face-based Extrude Join (including outward Press Pull) now defaults to the picked body,
so edge-touching assembly neighbours stay separate regardless of body order (#226).
In scope, in priority order:
1. SOLID and SKETCH (now).
2. Parameters / functional constraints (done: see `parameters.*`, `expr.evaluate`; unit-aware,
cycles reported with their path, rename follows references, deletes list their users):
user parameters with units and expressions (+-*/, ^,
parentheses, sin/cos/sqrt/min/max/floor/ceil/round, unit conversion), model parameters
d1, d2… for every dimension and feature input, references between parameters with a
dependency graph and cycle detection, re-evaluation on change, comments, favourites,
CSV/JSON import and export.
3. Components and assemblies with joints (M9).
4. SHEET METAL: flange, contour flange, bend, unfold/refold, flat pattern with DXF export, sheet
metal rules (thickness, K-factor, bend radius) (M10).
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
## Milestones
| # | Milestone | Status | Hours left |
|---|---|---|---|
| M0 | Vertical slice | done | — |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces, at distances from reference edges, and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), drawing snaps to where two lines, circles or arcs cross (#4967), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop, browser and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; Browser Expand All / Collapse All (`browser.expand_all`, `browser.collapse_all`) on the design and component menus; preferences | in progress | 6 |
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements, and exported as an animation coming apart and back together); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. macOS Developer ID signing and notarization are in the release workflow (#5749; they switch on when the Apple secrets exist). Windows installer signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
| M15 | Frames and weldments (#4003): structural members along 2D and 3D sketch lines, butt and mitre corners, trim to faces and bodies, end caps, gussets, cosmetic welds and a cut list. Done: the profile library (`frame.profile.list`, `frame.profile.properties`, `frame.profile.insert`): bar, round bar, rectangular and round tube, angle, channel and I-beam, each SolidCraft's own parametric definition with exact fillet arcs and exact section properties (area, centroid, second moments), tested against hand-computed values; members (`frame.member`): one body per straight line of a 2D or 3D frame sketch, following parameter edits, with a built-in profile (its dimensions parameter expressions) or a profile sketch (its drawn material by alternating nested loops), anchor, angle and mirror; each member's name kept with its line. Left: corners, trims, end caps, gussets, welds, cut list, FRAME tab and dialogs | in progress | 60 |
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
With the **Fusion** preset and two-finger pan enabled, two fingers pan and Shift + two fingers
orbit freely, with pinch zoom and mouse bindings unchanged. The shared navigation handler
applies the same gestures to model and Render views and blocks orbit on flat pages.
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
hierarchies are shared; bounded desktop preparation and occlusion use up to eight workers.
Curves are approximated; drawing sheets,
annotations and exact projected curves remain deferred. See [view DXF](docs/view-dxf.md).
Drawing foundations: [RFC #860](https://github.com/tangency-labs/solidcraft/issues/860)
and the [foundation ledger](docs/drawing-foundations.md) track the bounded implementation
packages. The first geometry increment defines validated section records in a plane-local
frame; the ledger separates that contract from section producers and the Drawing workspace.
Desktop view DXF export runs in the file-operation worker with five-stage progress and
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
completed drawings publish atomically. The browser still exports synchronously.
## Interactive modelling
- Modelled threads preview their cut before OK using the background worker on desktop.
Browser users apply the cut with OK to avoid blocking on every edit.
- Thread resolves a suggested metric size when a cylindrical face is picked. Diameter and
pitch selectors expose the existing metric designation parameter; custom designations stay
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
full/partial-length options remain available. This does not add tolerance classes or
left-hand threads.
- New Body extrusion and face-offset previews show completed intermediate geometry while a
drag continues, then catch up to the exact final value. Changes to the selected geometry,
operation or feature still discard obsolete results. Distance arrows and their value boxes
follow the current frame's input.
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
## Physical material data
- Physical material card validation (#923/#922) provides typed SI mechanical and
thermal data, per-property sources/conditions and isotropic/orthotropic stability
checks through read-only `material.properties.validate`. It does not assign or
persist properties, provide a solver or enable rendering; see
[the contract and staged follow-ups](docs/physical-material-cards.md).
- Directional elasticity (#994) queries the validated card in material-local axes
through read-only `material.properties.directional`, with source/condition review.
It does not assign body frames, predict structural behavior or enable a solver.
## Library foundations (#785, #786, #787)
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
selected appearance/material revisions into existing design definitions in one undo step.
Appearance offers local loading/search/export; physical materials use existing commands.
This slice is independent of the render stack and does not change the document format.
See [the contract and limits](docs/book/src/dev/local-libraries.md).
- Proposed phases cover offline bundled/project/user packs, physical materials and
appearances, editable wood mapping, and parameterized component/assembly families.
Stable identity/revision, copied design definitions, provenance and resource limits
apply from the first implementation. Existing material, appearance and standard-part
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
for source/license review and acceptance gates. This is planning only: pack loading,
texture rendering and expanded component families are not implemented by this entry.
## Render foundation
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
- Definition-local visual mapping (#1214) supplies a bounded numerical frame, physical repeat coordinates and a deterministic one-time grain-axis suggestion. It adds no saved record or texture consumer; see [the mapping contract](docs/render-visual-mapping.md).
- Explicit texture normalization (#1218) produces completed float RGBA with
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
gamut clipping only when explicitly requested. It is not a renderer/resource
consumer; see [the supported policy and limits](docs/render-texture-normalization.md).
- Opt-in opaque PBR (#1046) connects persisted surface parameters to the integrated
product viewport and CPU PNG, using a fixed direct-light studio. Geometry and mass
stay unchanged. HDRI, transmission and textures remain separate gates.
See [the consumer and limitations](docs/render-opaque-pbr.md).
- Combined integration is tracked in #842, with independent scalar libraries in
#863 and richer PBR/material-card transport in #1086. See [phase boundaries and validation gates](docs/render-integration.md).
Render/library consumers use the merged asynchronous picker (#1127). Material-card
UI (#1094), texture mapping and HDRI lighting remain explicit follow-ups.
- R4a adds bounded CPU product-image jobs, cancellation and atomic PNG export through
engine commands, the Render panel, CLI and persistent MCP. Immutable captures and
results stay outside persistence/undo; native workers and cooperative browser tasks
share the same work protocol. See [jobs, limits and export](docs/render-jobs.md).
- R3 adds an overlay-free product scene and the integrated Render workspace, with
runtime GPU host capabilities, per-document drafts and named camera/background
controls. Design keeps its camera and drawing policy. Raster transparency remains
approximate; PBR and environments are later slices. See
[workspace and scene contracts](docs/render-workspace.md).
- R2 adds bounded named render setups in document format 21 and the read/save
`render.settings.get/set` commands. Proven render-only changes preserve geometry
and prepared world data through undo/redo and checkpoint restore. This slice does
not add a Render workspace, PBR, environments or image jobs; see
[settings and refresh policy](docs/render-settings.md).
- Bounded PNG/JPEG texture admission (#900) validates in-memory sources and returns
completed RGBA8 pixels with explicit color/data roles. It is an input utility only:
ICC/16-bit normalization, local wood mapping, resource persistence and the renderer
consumer remain separate work. See [limits and licensed-file probes](docs/render-texture-input.md).
- The initial opaque surface numerical contract for #1004 provides bounded linear
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
The numerical module alone advertises no capabilities; the installed consumer above
provides rendering. See
[the model, evidence and remaining consumer gates](docs/render-surface.md).
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
it does not yet enable HDRI/PBR rendering or a document import command. See
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
bounded image/scratch admission checks, exposed by the read-only `render.backends`
command. It describes the current CPU raster snapshot path without evaluating the model.
The legacy Design snapshot route remains unchanged. Later slices above provide the
workspace/jobs and opaque PBR; HDRI and optional engines remain future work. See [the contract](docs/render-backends.md)
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
## Dimensioning and error reporting
- Operation length fields share design-unit defaults and input evaluation, including secondary
distances, thin walls, blend controls, sheet/plastic rules, joints and motion keys. Canvas
handles and sketch previews use the same lengths as committed geometry. Existing stored
millimetre values retain their size when edited in a design using another unit (#3271).
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
Both tools retain full replies by default.
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
Polygon and Spline. Each open part remembers its last interactive choice per family
across sketch sessions and document-tab switches; closing the part resets these UI choices.
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
placement, including orientation and the selected side count.
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
dimension values, so line type can change while a shape is in progress.
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
committed geometry; centerlines retain their dash-dot pattern.
- Point has a default Sketch CREATE toolbar button alongside the existing drawing tools.
Saved custom layouts are preserved; Reset Toolbar restores the default Point button.
- Construction planes cannot take an origin-plane name (XY, XZ, YZ, Top, Front, Right). Plane
arguments reject such a name when an older design's construction plane already has it. Use
the other alias, the construction feature's numeric ID or an explicit plane; existing
stored references keep their saved meaning. Section Analysis uses the same check.
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
Interior selection requires every profile boundary to belong to the same text source;
the editor highlights the whole source without replacing profile selections.
Content, baseline X/Y, height and rotation update the original text source in one undo step;
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
use the document units captured when editing starts; generated outlines remain associated
with their text source, and untouched inputs preserve their saved internal values.
Dragging a text outline moves the entire source in one undo step; Escape restores the
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
about its baseline; copies keep an independent editable text source.
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
waits for a distinct point without a degenerate-shape error or history entry. Typed input
and programmatic commands retain their validation.
- Circular-pattern and angled-plane axes reject explicitly supplied malformed or expression
origins instead of silently rotating about zero. Numeric origins remain in millimetres;
associative axis expressions and construction-axis references remain unsupported.
- Single-line and rectangle-side dimensioning supports selecting a line or its two endpoints.
Type a value and press Enter to commit one undo step; rejected values remain editable and
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
and navigation controls, including enlarged displays and nested pending placements.
- Sketch Dimension takes a line and a circle or arc (#1803): it measures from the line to the
circle's edge (the gap, 0 when tangent, or how far the edge reaches past a line that cuts
it), drives it like any other dimension and keeps the circle on its side of the line.
- Sketch Dimension takes a point and a circle or arc, or two circles or arcs (#4064): it
measures to the circle's edge (from the point, or between the two edges: the gap, 0 when
they touch, how far they overlap, or the narrowest gap between nested circles).
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
the current design unit; changing units leaves existing geometry and expressions intact.
Component/canvas distance controls, sketch cursor coordinates, flat-pattern dimensions
and pocket depths, interference volumes and parts-library lengths also use design units.
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
consumes complete point/curve selections consistently; mixed or ambiguous selections
retain the picking workflow. Rejected constraints preserve selection and undo history.
- Contextual error reporting: a **Create Issue** action beside feature errors opens a
reviewable GitHub issue draft with the error, operation settings, build diagnostics and
useful reproduction context. The draft is editable, with optional capture of the actual
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
reporting for unexpected results that did not produce an error.
- The header exposes **Report a bug**, including an accessible compact control on
narrow windows. It opens the same editable report and screenshot review without
changing the active model or a pending dimension.
- Crash reports: every panic (in a command, in the kernel, anywhere else) and every unclean
exit (abort, stack overflow, kill: found at the next start through a locked per-run command
journal) leaves a local report with the backtrace, version and commit, the command and the
design that reproduce it (`solidcraft-cli crashes replay <id>`). Repeats are counted, not
rewritten; at most 20 reports, 50 MB and 90 days. The app shows them after a crash (Help >
Crash Reports…) with a reviewed issue draft; agents read them through
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
anywhere. See [crash reports](docs/crash-reports.md).
- Agent chat (off by default, desktop): a side panel that drives a coding agent the user
already has (Claude Code through its ACP adapter, Codex, Gemini CLI, OpenCode, Grok Build, …) over the
Agent Client Protocol, under the user's own login; SolidCraft stores no keys. The agent works
through SolidCraft's MCP tools on a gated endpoint of its own: file actions wait for Allow or
Deny, crash reports are off limits, 24 changes per message, one undo step per message, and
the agent's whole process group ends with the panel or the app. Docks left or right,
minimises to a strip. Grok Build discovery includes `~/.grok/bin` and reuses the user's
Grok login. See [agent chat](docs/agent-chat.md).
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
writes a design's history as a script that `eval` and `run` replay into the same design —
user parameters, units, sheet metal and plastic rules and materials as their own commands,
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
script after a step gives the design at that step. Appearances, folders, views and
configurations are listed as left out; components and joints are not supported yet.
## 0.2.0 release batch
- Includes the new axis navigation, adaptive sketch drawing/picking, origin-corner repairs,
durable supported profile/face references, endpoint dimensioning and contextual reporting.
- Smooth ruled-wall display joins are hidden only within the supported geometric certificate;
authored splits and actual creases remain protected. Wireframe retains physical edges.
- Paste New uses a bounded current-geometry comparison for supported convex line/circle
prisms with separated circular holes; unsupported shapes retain the existing strict check.
- Common-bore Union retries check additional boundary curves before recutting the bore,
preventing the reproduced thin-web loss. Unknown additional trims refuse; inherited
common-cylinder trims retain their existing policy, so this is not a general Boolean proof.
- Exact full-path assembly joint editing remains separate work and is not part of this batch.
- Stable 0.2.0 is published: all release payload checksums, native cold reopening, deployed
web app and both Flatpak architectures were verified. The replacement README demo uses
the original electronics enclosure story and animated pointer in the actual English
0.2.0 UI, at 4× playback. Publication follows [the release workflow](docs/releasing.md).
## 0.2.1 critical fixes
- Separated multi-profile Join/Cut extrusions into one solid target batch their tools using
conservative construction bounds, avoiding a Boolean rebuild for every profile. Live
extrusion previews build only their tools; confirmation validates the final Boolean.
- Coplanar selected body faces share one linked sketch and one extrusion. Automatic Join/Cut
keeps the evaluator's participant scope, including parts of an empty imported component. Desktop OK/Enter
computes on a scratch session in the background, with atomic acceptance and cancellation.
Planar section stitching projects lines/arcs analytically and removes duplicate endpoints.
- Imported rotated face groups can check all full start-face seams in one guarded
construction. Separated tools may batch a Join when each meets the same single body;
nearby uninvolved bodies retain the original contact decisions. Regression comparisons
cover all four extrusion operations.
- Inward multi-face cuts retain every eligible target and subtract separated tools together.
Safe interior classification handles roundoff at shared imported line/arc endpoints.
Straight extrusion feedback caches existing face/profile triangles and updates the display
directly, including a newly opened STEP's single identity component placement; other
extents, moved or repeated components and curved sketch profiles keep the Engine preview.
- The extrusion fixes retain tracked Boolean history and the current `solidcraft` packages.
Confirmation and tool previews use main's shared background calculation. Main's collapsed
blend-wall refusal and its tests are retained, including smaller valid radii and mirrored parts.
- Axis view and navigation controls stay anchored to the viewport when feature dialogs or
the command palette open, at normal and enlarged display scales. Fit still accounts for
dialog coverage; covered controls do not receive clicks through dialogs.
- Eligible planar faces survive fillet, chamfer and shell operations with exact source
certificates. Sketches placed directly on the enclosure wall and inner floor follow
width and wall changes, including after the USB cut and cold reopening.
- Remove artificial straight-wall and rounded-corner subdivisions left by shell and
extrusion cuts. Identical generated wall segments span adjacent slabs as one physical
face, with exact full-height supports and boundary stitching. Preserve authored face
splits and face appearances. Permanent regressions cover material, STEP, reopening and
unaffected bodies across the extrusion end plane.
- Feature dialogs use the available screen height for their fields, with OK and Cancel
outside the scrolling region, including enlarged display scales.
- Design format 13 records shell-face attachments. Older designs remain readable;
newly saved designs require 0.2.1 or later. Ambiguous or unsupported faces still refuse
attachment rather than guessing a plane.
- Saved projections from a removed strip retain their geometry and report the lost
reference. Explicitly reselect the wall to repair that projection.
## Workspace layouts (#2029)
- General preferences save Floating or Compact using the universal UI containers.
Compact joins the workspace selector and ribbon, docks the content-hugging Browser
flush beneath them with a square join, and opens tool sheets below the compass.
Sheets remain draggable, rounding all corners away from their right dock; footer
backgrounds follow the same corners. A separate preference hides ribbon icon labels
for a shorter ribbon in either layout.
Assistant keeps the outermost edge and History keeps its chosen dock. Switching
retains the active tool, camera, selection and design; system windows and menus
keep their existing behavior.
## View navigation
- Navigation has an opt-in Free orbit preference for mouse and 3D-mouse rotation through
both poles with roll. It defaults off, preserving existing preset snapping and pole limits.
The SolidWorks face menu offers Normal to Selection (`view.normal_to`), shared with Look At
and Ctrl+8, including repeat-to-flip, preserving scale and projection.
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
its layout stable during a drag and applies the selected scale on release.
- OpenSCAD navigation preset: left drag orbits, right/middle drag pans, Shift+right/middle
drag zooms, and the wheel zooms. Available in the shared desktop/web Navigation preferences.
- Compact clickable signed X/Y/Z axis tips, smooth drag-to-orbit, and Shift-orbit snapping
to 26 standard and diagonal views. Pointer motion accumulates independently of a lightly
damped spring, with a boundary margin to prevent flicker. Z-up/Y-up and the nearest square
top/bottom heading are preserved. Diagonals include 45-degree edge and isometric corner views.
## Known limitations
- Scheduled and manual CI scan locked dependency advisories, bans and sources with
cargo-deny. Vulnerabilities fail the scan; unmaintained crates warn. Local vendored
patches require separate upstream review because advisory databases cannot version-match
their path dependencies. Pull requests retain the existing licence checks.
- Associative shell-face sketches require a unique certified planar source. Nested shell
attachments and ambiguous coincident sources remain unsupported.
- G2 fillets: exact quintic blends of straight edges between planar faces, with one planar
end face at each end, including sloped ends (#2708). The UI and `solid.fillet` expose
`curvature` (alias `g2`); the size is an equivalent circular setback. Curved faces/edges,
multi-face ends and corner junctions remain unsupported.
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
meet (a sphere for equal radii); perpendicular corner fillets after a closed ring of earlier
rounds preserve the ring's radii and the enclosed planar face (#1557);
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
straight edges (convex or concave) between planar faces with perpendicular
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
corners around pocket rims, with unequal wall slopes and extensions confined to the face;
connected equal-distance chamfers also trim each other across unequal sloped rim walls;
curved chamfer chains with straight planar ends can terminate across two existing planar
bevel faces, splitting their shared seam; picks on the same tangent chain are applied once (#1303);
whole smooth loops of a planar face, picked whole or by one edge (tangent
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
plate outlines), loops with sharp corners between straight or mixed arc/line edges (straight
blends meet in mitres; perpendicular curved walls use offset intersections, with D-rim removal
checked within 0.1% of analytic offset slices), runs of
neighbouring edges along a planar face's loop (two top edges of a box: mitred, ending square;
with the edge between them too, the corner becomes a sphere octant),
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
between two overlapping extruded arcs); knife edges, where the two faces have opposite
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
other, except a round after a round of the edge between the wall and a flat face that it
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
piece of sphere. A limiting blend that collapses a side wall into coincident boundary
curves is refused as too large rather than returning an unmeshable face (#1113).
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
curved wall qualification rejects false transverse axes instead of filling holes.
Bodies touching along one whole face
(a part and its mirror image) are stitched; coincident planar faces are pushed apart when
their neighbours are planes or cylinders. Other coincident curved configurations can still
fail. Bounded containment classification preserves fully internal cavities and nested
material islands independently of shell order, with STEP round-trip and material-membership
regressions. Near-contact curved shells use the general boolean path rather than a
containment shortcut. Spheres are built from six pole-free patches.
- Mass properties come from fine tessellation (curved faces within ~1e-4 relative); triangles
that cut through curved faces (trimmed faces after booleans) are split onto the surface first.
- STEP import (our own reader, crates/kernel/src/step_in) reads solids (B-rep with analytic and B-spline geometry), units,
product names, colours and assemblies; analytic offsets on planes, cylinders, cones, spheres
and regular tori are read exactly (signed/nested distances, units and face sense). Freeform
offset surfaces and pcurve-only edges are not read yet,
and assembly components are flattened into bodies. Closed periodic faces (torus, B-spline
bands) are split in two for meshing; measure() uses a finer tolerance for small radii.
- Persistent naming follows the kernel's exact face history (docs/book/src/dev/naming.md):
references resolve by name first, follow splits and merges, and warn when the entity split,
merged or vanished, or was found by geometry only (operations that report no history yet).
New planar sketch attachments use exact box, extrusion cap/side and immutable imported-face
source certificates with checked target and author occurrence paths. A missing or changed
source fails explicitly; Redefine Sketch Plane previews reattachment or a fixed plane while
retaining current geometry and dimension identities. Unsupported modified faces need an
explicit fixed plane. Legacy attachments retain their original resolver behavior.
- Modelled threads are cut as a faceted body (curved faces within 1/200 of the pitch); Extrude
and Revolve To Object on a curved face cut the tool back to the target's surface.
- Explicit sketch breaks preserve exact ellipses, fit/control splines (including high degrees)
and conics; analytic trim/break also sees free-form cutters. Free-form curves cut at a tangent
touch (no clean crossing) enter that profile as polylines.
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
A sweep with a twist, an end scale or a parallel or direction orientation (#771) follows its
path that way too, even along sketch lines and arcs: B-spline walls through stations, ruled
walls through denser stations where the path has corners or the profile has holes.
Guide rails (one or two, #771) do the same: each station meets every rail once, and the rail
turns the profile and scales it (or stretches it across to the rail) against the start; two
rails carry it between them. Rails along a path with sharp corners are refused for now.
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
certificate and refine within bounded node limits while retaining existing IDs.
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
speeds without changing stored nodes or relaxing the distance tolerance (#971).
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
editable source updates the derived boundary live. Break Association restores point editing.
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
Newly captured adaptive profile references with complete source coverage retain their outer
and hole identities through span-count changes, including parameter edits after extrusion.
Partial, intersected or ambiguous boundaries do not qualify for these whole-source references.
Legacy curve-span multisets remain exact and unchanged; already-lost selections may need
reselection. Format 12 stores the new bounded whole-boundary references.
Projected analytic boundaries follow nested loop identities through resizing, including
stationary holes and rounded corners. Supported convex, tangent line-and-arc boundaries
can lose or regain corner arcs during an inset while preserving reserved curve identities
and linked profile selections. Format 12 saves those associations; compatible older closed
offsets upgrade structurally without changing their curve IDs. Breaking an association
freezes its current boundary and materializes downstream profile references. Child offsets
of a boundary with retired arcs still require breaking its association first; mixed or
nonconvex junctions and retained references to disappearing individual arcs remain limited.
- Extrude and Revolve editors retain original direction, extent expressions, explicit targets,
axes, From/To inputs and All-profile intent. No-op edits preserve downstream dimension names
and selected occurrence frames; cancel and successful edits preserve one-step undo history.
Placed feature edits preview through the edited timeline entry, and cylindrical Emboss guides
use the sketch's actual target, author or uniquely placed third-component frame.
Revolve rotators follow the actual signed axis and support symmetric and two-sided extents;
dragging a To Object extent converts its derived value to an explicit angle. Legacy reversed-axis
symmetric geometry is preserved through format 11's explicit legacy convention. New revolves
use the sketch-normal direction for each selected profile, including To Object and curved
target trimming, independently of the selected axis sign.
- Cylindrical emboss/deboss wraps bounded orthogonal line profiles, including holes, concave
outlines and nested islands, onto complete exterior cylinders or coaxial sleeves with exact
radial walls. New features infer a unique eligible cylinder when the face field is empty;
ambiguous targets require an explicit face. Saved target
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
between sections, and sections with holes cannot collapse to a point.
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
the origin; clicked UI regressions verify geometry, closed profiles and undo.
- Sketch display: drawing, picking, highlighting, tool previews and profile shading share
adaptive projected curve boundaries. Fit, zoom, display scale and placed sketch frames
invalidate the display cache without changing CAD geometry or timeline history. Work
budgets and clipped paths prevent unbounded sampling or invented edges across the eye plane.
The Sketch Palette scrolls its options on short windows and enlarged displays while
keeping constraint status and Finish Sketch visible.
- Face projection: closed boundaries have a deterministic outer-first order. Refresh matches
existing geometry before assigning positions, retaining saved curve IDs and constraints
across enumeration changes and resize, including concentric circles. Ambiguous identities
retain the last geometry and report a lost reference; permutation matching is bounded.
- Project accepts complete supported preselection in one undo step. Visible external sketch
curves and points retain their source sketch identity; model geometry, axes and planes use
the existing linked projection commands. Rejection preserves the document and selection;
mixed selections retain ordinary tool-first picking.
- Inline sketch dimensions: Tab and Shift+Tab cycle within the active shape's value boxes,
selecting visited values for replacement. Expression completion retains forward-Tab
priority; real keyboard regressions cover rectangles, lines and single-value circles.
Typed lengths, radii, line angles, signed arc sweeps and slot lengths persist as constraints
and expressions through save/reopen. Shape creation and its dimensions form one undo step;
rejected values keep the editable field focused.
- Repeated components: Browser selection, visibility, properties, copy and Move/Copy use
checked occurrence paths. Active nested occurrences author planar sketches and solids in
their selected frame. Pending moves compose through parents; dialog cancellation restores
its selected move. Source materials and body/face appearances follow transformed instances.
Shared nested definitions remain shared. Deep Paste New copies reachable definition graphs,
independent feature/input histories, styles and internal motion records, with bounded geometry
checks and explicit external-link notices. Paste New selects the copy while preserving the active
authoring context. Imported periodic STEP seams survive placement and reexport with shared
topology and appearances intact. Recorded body transfers preserve historical Join/Cut membership,
material, placements and machining data through edits, copying, undo and reopen. New 3D wires
record their coordinate frame; legacy wires migrate from known provenance without changing
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
See [occurrence workflows](docs/occurrence-workflows.md).
- Toolbar drops on panel labels, gaps and margins append commands with insertion feedback;
button drops keep insert-before ordering.
- Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel.
Parameters opens the user/model variable editor; Configurations opens the configuration table.
Configure remains available in the panel dropdown.
- Parameter creation supports Enter with validation and continued keyboard entry. Shared
expression autocomplete supports Up/Down selection, Enter/Tab acceptance and Escape dismissal;
accepting a suggestion does not submit a parameter or trigger viewport shortcuts.
- Movable window title bars follow the app's dark/light header palette. Radial right-click
menus are opt-in in Preferences > Navigation; ordinary context menus remain the default.
- Desktop distribution: running Windows windows and executable resources use the app icon;
packaging validates icon resources. Nix installs launchers, MIME data and generated icons,
with explicit launch apps and Vulkan-loader/driver discovery. Flatpak feed instructions
register the required runtime source; release checks exercise desktop modelling on X11/Wayland.
- OpenGL rendering clamps effective anti-aliasing to 1x across UI pipelines, textures and CAD
callbacks to prevent black frames. Other backends retain the requested sample count, and
stored preferences remain unchanged.
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
restrict values. Preview explains effective values without modifying the document; applying
rules validates rows atomically. Length expressions retain their authoring units across
document-unit changes. The UI supports structured All/Any/Not conditions and rule ordering.
Component styles and certified semantic face styles are available; unsupported lineage refuses
atomically, and stale certified face paint cannot jump to another surface. See [configurations](docs/configurations.md).
- Sheet-metal material: inherited holes, crossing edge notches, overlapping cuts and retained
islands share the same boundaries in flat preview, DXF and unfolded solids. Detached material
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
sampling.
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
exact holes and retained islands, including the union of multiple selected profiles, and
records complementary depth layers as pockets while retaining the stock gauge. Unsupported
tapered or bend-crossing machining refuses atomically instead of restoring material later.
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
clear of the flange beside it, and the returns of one feature are mitred toward each other
with the rule's gap where their flanges meet at a corner; returns of separate features, and
flanges on returns, are not closed toward each other.
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
check. Under-resolved fits return an error.
- Contact driving checks the trajectory between clear endpoints, including linked joints,
playback and sparse motion exports. Checks use bounded tessellation samples, not exact
continuous collision detection; local thin walls and small details remain limitations.
See [contact-set limits and recovery](docs/contact-sets.md).
## Assembly dragging
The design's first part is grounded when it is made, and a left-button drag repositions a free part (`occurrence.drag`: one undo step, exact occurrence paths including repeated jointed definitions, any floating-chain member carries the chain, grounded parts and joints hold where present, Escape cancels).
## 3D sketch bridges
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
the active plane. Bridge endpoints capture their source positions; associative endpoint links,
construction-plane selection of 3D wires and loft guide rails remain separate work.
Feature dialog header chevrons collapse and restore the form without discarding selections
or edited values, including the Appearance panel.
## Construction on 3D paths
Plane Along Path has a viewport path selector, fractional position, live preview and timeline
editing. Construction planes and points resolve drawn 3D wires without applying their sketch
plane a second time. Curved paths use sampled segments; multi-curve chains, absolute distance
and reference-object placement remain separate work, as do loft rails and centerlines.
## Design properties (#1015)
- Designs carry project, part number, revision, author, organization, description and licence
in an optional `metadata` object (File → Design Properties…, `document.metadata.*`,
`inspect_design`). STEP and IGES headers and 3MF model metadata take the fields their format
has a place for. Other tools keep their own data under a reverse-DNS key, kept as it is
through open, edit and save and never exported. See [design properties](docs/book/src/guide/files.md#design-properties).
Per-component properties (a part number per component) can follow with the same type.
## Document-opening performance
- Desktop STEP import prepares consecutive independent solids in bounded batches with up
to eight workers, retaining serial traversal, result order and persistent source identities.
The web path remains serial. Original multi-solid fixtures show 64–81% lower complete
import times; single-solid assembly parts are not scheduled across nodes. See
[measurements and limits](docs/perf.md#parallel-step-body-preparation-2026-10-12).
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
format. Generated-mesh storage measurements reduce serialization and decoding work;
a seekable reader defers original blobs until after provisional display.
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
and geometry records, with a separate truck adapter and bounded binary reader. Exact
face-source certificates and operation history survive codec round trips. A provisional
picture can be generated before original blobs are read; editing and measurement wait
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
the same document despite concurrent file writes. Default saves omit this cache and never force cold
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
Keys, resource budgets and document/revision guards reject incompatible, hostile or
stale results. A second modelling kernel still requires its own tested adapter.
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
Home reads it without rebuilding the design when its local picture is missing;
mismatched or malformed thumbnails fall back to normal generation. The image is
kernel-independent and saving never forces cold meshing to produce it.
- Cold rebuild profiling separates parsing, geometry, placement, display meshing and
thumbnails. Straight-line closest-point projection and exact curved-surface evaluation
reuse reduce repeated work without relaxing geometry checks. Three alternating optimized
benchmark runs reduce median rebuild time by 26% on the public sensor enclosure and 22%
on a local case assembly; shape measurements and topology match the original build.
See [performance measurements](docs/perf.md). Further cold Boolean and import costs remain.
- Prepared assembly geometry survives selection and view changes while real geometry and
placement changes invalidate it. A Pi assembly edge-selection probe drops from 9.24 s to
0.57 s; initial opening is unchanged. Actual Windows desktop profiling measures 15.87 s
median opening, with 82% spent rebuilding geometry; halving full opening remains open.
## Kernel
- Successful Boolean retries report their strategy and numerical adjustments as
feature warnings and structured inspection data. Diagnostics survive cached
evaluation, undo/redo and cold reopening; the robustness runner counts retry
events by strategy and reports any bounded-log truncation. Existing geometry
acceptance checks remain in force.
- Cylinders of one radius whose axes cross (a second bore across a bore of the same size, a pipe
tee of equal pipes) touch at two points, where the mesh intersection cannot trace the curves
and a boolean cannot tell the four arms of the crossing apart. When the ordinary boolean fails
on such a pair, both faces are split along the line of their cylinder through the touching
points (the solids are unchanged), the boolean is taken of those, and its history is composed
back over the operands as given (#788). The result keeps the split lines as edges of the bore
walls. A crossing the ordinary route fails on and the split faces do not help (a radius of 17.5
in a 45 mm block with a tool of 80 mm) is still refused.
- Boolean intersection-curve construction refuses point-only, collapsed and non-finite
polylines without panicking. Direct constructor regressions retain valid open endpoints
and closed spherical intersections; the original interactive trigger remains unidentified.
- Shell of a stepped, joined planar body no longer creates horizontal subdivisions across
continuous walls. Fresh axis-aligned Cut walls are stitched using their exact construction
support and shared topology, preserving real steps and intentional boundaries. The local
reported case returns from 86 to 49 faces with unchanged material and bounds; regressions
cover stepped stock, STEP round trips and saved projections from removed pieces.
truck (Apache-2.0) behind our own `kernel` crate, with our own blend operation and boolean
retry/validation; see `plan/adr/0001-geometry-kernel.md` (local) — summary in README.
The short version, for people who use SolidCraft. The detailed developer status (milestone
tables, limitations, measurements) is in [docs/roadmap-detail.md](docs/roadmap-detail.md).
## What SolidCraft is
SolidCraft is open-source parametric 3D CAD written in Rust. You sketch, constrain, extrude, and
change a parameter to watch the timeline rebuild. It runs as a desktop app and in the browser
([try it](https://solidcraft.io/app)), and it has a command-line tool and an MCP server so that
scripts and AI agents can drive the same commands you use.
## Works today
- Sketching with a constraint solver, and the usual create and modify tools.
- Extrude, revolve, loft, sweep, coil, fillet, chamfer, shell, draft, holes, threads,
patterns, mirror and split.
- Parameters with units and expressions, and configurations.
- Faces and edges keep their names through edits, so references survive changes.
- Components, assemblies, joints, motion studies and exploded views.
- Sheet metal with flat patterns and DXF export, and a plastic-enclosure subset.
- A Render workspace with GPU rendering, studio lighting, materials and image export
(first release in 0.22.0).
- Import and export of STEP, IGES, 3MF, STL, OBJ and DXF, and FreeCAD and Rhino files.
- Autosave and crash recovery, and the browser build of the same app.
The full list of commands is in [docs/features.md](docs/features.md). Release notes for each
version are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
## In progress now
Each feature below is a [milestone](https://github.com/tangency-labs/solidcraft/milestones) holding
the issues for it, so the count shows how far along it is. Counts are as of 2026-10-25, one per
feature or request. Work merged on a feature branch (`drawings`, `render`) counts as built but
reaches the app only when that branch merges. They are rough, not a promise.
- **[Rendering & materials](https://github.com/tangency-labs/solidcraft/milestone/17)**: in progress, 9 of 34 pieces done, 3 of them built only on the `render` branch and in the app when it merges. The basic Render workspace is already in releases (0.22.0); the interface rework (#6281) and the material and light libraries are what is left.
- **[Drawings](https://github.com/tangency-labs/solidcraft/milestone/8)**: in progress, 3 of 21 pieces built (2 in releases, 1 on the `drawings` branch), and 3 more partly built there (sheets and borders, bill-of-materials types, the paper scene). Not in the app yet: it lands when the `drawings` branch merges. Today you can export the current view as a DXF.
- **[Sheet metal](https://github.com/tangency-labs/solidcraft/milestone/15)**: in progress, 1 of 30 requests done. Flanges, hems, unfold and flat patterns already work; the 30 are extensions, mostly the per-edge flange options (segments, miters, partial flanges, reliefs) and bend allowance.
- **[Sketching](https://github.com/tangency-labs/solidcraft/milestone/14)**: in progress, 12 of 50 pieces done. Dimension types, selection and snapping options.
- **[Solid modeling](https://github.com/tangency-labs/solidcraft/milestone/2)**: in progress, 9 of 50 pieces done. Blends, holes, threads and patterns.
- **[Assemblies & joints](https://github.com/tangency-labs/solidcraft/milestone/3)**: in progress, 4 of 19 pieces done. Joints, motion studies and exploded views already work; placement rules, linked sub-assemblies, fastener stacks and weldment corners are the pieces left.
- **[Import & export](https://github.com/tangency-labs/solidcraft/milestone/5)**: in progress, 7 of 39 pieces done. FreeCAD, SolidWorks and point-cloud import, and mesh export options.
- **[AI agents & MCP](https://github.com/tangency-labs/solidcraft/milestone/18)**: in progress, 2 of 8 pieces done. Agent chat and the MCP server.
- **[Interface, web app & plugins](https://github.com/tangency-labs/solidcraft/milestone/4)**: in progress, 31 of 96 pieces done. Ribbon redesign, navigation, themes, the web app and plugins.
- **[SMLib kernel migration](https://github.com/tangency-labs/solidcraft/milestone/1)**: in progress, 1 tracking issue open. The kernel is moving from truck to NVIDIA's SMLib (usd-brep, Apache-2.0) on the `kernel-smlib` branch; it is not in releases yet. Tracked in [#4012](https://github.com/tangency-labs/solidcraft/issues/4012).
## Next
- **[Surfacing](https://github.com/tangency-labs/solidcraft/milestone/16)**: next, 6 of 14 pieces done. Patch, trim, blend and match exist; the rest of the surface toolset is planned.
- **[Manufacturing / CAM](https://github.com/tangency-labs/solidcraft/milestone/9)**: next, 1 pieces tracked, none done. There is no CAM in SolidCraft yet. Send to Slicer and a first toolpath slice are in review; proposals are [#1153](https://github.com/tangency-labs/solidcraft/issues/1153) and [#1621](https://github.com/tangency-labs/solidcraft/issues/1621).
Features are not tied to a release or a date. Work lands in the daily releases as it is finished.
Anything without a milestone, such as bug fixes, is not a feature and is not listed here.
## How releases work
- Releases ship daily, as numbered versions (v0.22.0, v0.22.1) with nightly pre-releases in
between. They are delivered through the in-app updater.
- SolidCraft tells you when a newer stable release exists and never updates without asking.
Where it can replace itself safely (AppImage, Windows installer and portable builds) there is
an **Update now** button; downloads are checked against the release's SHA-256 list. See
[updates and privacy](docs/book/src/guide/updates.md).
- Design files carry a format number. A new version opens designs from older versions, but an
older version refuses a design saved by a newer one (0.22.0 writes format 35, and 0.21.0 and
earlier will not open it). Each release's notes say when the format changed. Keep a copy of
an important design before you move to a new version.
## Licence
SolidCraft is licensed MIT OR Apache-2.0, at your option; see [LICENSE-MIT](LICENSE-MIT) and
[LICENSE-APACHE](LICENSE-APACHE). Versions already released stay available under those licences
permanently.
## How to follow along
- Feature milestones: <https://github.com/tangency-labs/solidcraft/milestones>
- Issues: <https://github.com/tangency-labs/solidcraft/issues> (area labels such as
`area:kernel`, `area:sketch` and `area:ui` group them)
- Discord: <https://discord.gg/FtU4NQNVDH>
- Releases: <https://github.com/tangency-labs/solidcraft/releases>
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
+4 -3
View File
@@ -262,8 +262,7 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
if solidcraft_engine::io::is_design_container(&bytes) {
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
let mut session = Session::new(doc);
session.path = Some(path.to_owned());
let mut session = Session::opened_at(doc, None, Some(path));
session.param_check = check;
return Ok(session);
}
@@ -340,6 +339,7 @@ fn print_json(v: &Value, file: Option<&str>) -> Result<(), String> {
fn cmd_run(args: &[String]) -> Result<(), String> {
install_renderer();
let json_out = json_out(args)?;
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
@@ -444,6 +444,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
let mut s = load(path)?;
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
@@ -843,7 +844,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
return Ok(());
}
let t = std::time::Instant::now();
let s = Session::opened(doc, saved.as_ref());
let s = Session::opened_at(doc, saved.as_ref(), (!step).then_some(path.as_str()));
drop(read);
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
// only the features whose state a thinned file left out.
+39 -2
View File
@@ -395,8 +395,10 @@ fn actions(s: &Session) -> Vec<Action> {
let r = round3((shortest * 0.1).max(0.01));
let mut add = |action, act| out.push(Action { entity: entity.clone(), action, act });
if !smooth(e, mid) {
add("edge.fillet", Act::Run("solid.fillet", json!({"edges": [pt(mid)], "radius": r})));
add("edge.chamfer", Act::Run("solid.chamfer", json!({"edges": [pt(mid)], "distance": r})));
// The midpoint is interior to this edge, but other bodies (including component
// copies in their local frames) can have coincident edges. Retain the picked body.
add("edge.fillet", Act::Run("solid.fillet", json!({"body": world, "edges": [pt(mid)], "radius": r})));
add("edge.chamfer", Act::Run("solid.chamfer", json!({"body": world, "edges": [pt(mid)], "distance": r})));
}
let dir = straight(pts);
if dir.is_some() {
@@ -1278,6 +1280,41 @@ fn rectangle(plane: Value, p0: [f64; 2], p1: [f64; 2]) -> [Value; 3] {
]
}
#[test]
fn edge_round_actions_keep_the_picked_body() {
for (corner, length) in [([10, 0, 20], 4), ([0, 0, 0], 20)] {
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Plate"})).unwrap();
s.execute(
"solid.box",
&json!({"corner": corner, "length": length, "width": length, "height": length, "body_name": "Block", "operation": "new"}),
)
.unwrap();
let mut tried = 0;
for a in actions(&s) {
if !a.entity.starts_with("Plate/") || !matches!(a.action, "edge.chamfer" | "edge.fillet") {
continue;
}
let Act::Run(id, ref p) = a.act else { continue };
let q = &p["edges"][0];
let x = q[0].as_f64().unwrap();
if q[1] != 0.0 || q[2] != 20.0 || !(0.0..20.0).contains(&x) {
continue;
}
// Block either meets this midpoint or coincides with the whole edge.
assert_eq!(p["body"], "Plate", "{}: {p}", a.action);
assert_eq!(q, &json!([10.0, 0.0, 20.0]));
let mut unscoped = p.clone();
unscoped.as_object_mut().unwrap().remove("body");
let err = s.scratch().execute(id, &unscoped).unwrap_err().to_string();
assert!(err.contains("edges meet"), "{err}");
perform(&mut s.scratch(), &a.act, &errors(&s)).unwrap();
tried += 1;
}
assert_eq!(tried, 2);
}
}
#[test]
fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
for (cmd, size) in [("solid.fillet", "radius"), ("solid.chamfer", "distance")] {
+99
View File
@@ -22,6 +22,19 @@ impl Scratch {
std::fs::write(&path, json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}}]}).to_string()).unwrap();
path
}
fn saving_script(&self) -> (PathBuf, PathBuf) {
let path = self.0.join("saving.json");
let saved = self.0.join("script-ran.solidcraft");
std::fs::write(
&path,
json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}},
{"command": "file.save_as", "params": {"path": saved}}]})
.to_string(),
)
.unwrap();
(path, saved)
}
}
impl Drop for Scratch {
@@ -43,6 +56,92 @@ fn body_count(path: &Path) -> Value {
json_output(cli().arg("eval").arg(path).output().unwrap())["body_count"].clone()
}
#[test]
fn run_rejects_invalid_options_before_script_execution_or_output_writes() {
let dir = Scratch::new();
let (script, script_saved) = dir.saving_script();
let cases = [
(&["--out"][..], "--out needs a value"),
(&["--save"][..], "--save needs a value"),
(&["--in"][..], "--in needs a value"),
(&["--out", "--quiet"][..], "--out needs a value"),
(&["--ouut", "part.step"][..], "doesn't take --ouut"),
(&["--unknown"][..], "doesn't take --unknown"),
];
for existing in [false, true] {
let saved = dir.0.join(format!("{existing}.solidcraft"));
let exported = dir.0.join(format!("{existing}.step"));
let report = dir.0.join(format!("{existing}.json"));
let outputs = [&saved, &exported, &report];
if existing {
for path in outputs {
std::fs::write(path, b"existing output").unwrap();
}
}
for (extra, message) in cases {
let output = cli()
.arg("run")
.arg(&script)
.arg("--save")
.arg(&saved)
.arg("--out")
.arg(&exported)
.arg("--json-out")
.arg(&report)
.args(extra)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1), "{extra:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains(message), "{extra:?}: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.stdout.is_empty());
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
for path in outputs {
if existing {
assert_eq!(std::fs::read(path).unwrap(), b"existing output");
} else {
assert!(!path.exists());
}
}
}
}
}
#[test]
fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting() {
let dir = Scratch::new();
let (script, script_saved) = dir.saving_script();
let cases = [
(&["--in"][..], "--in needs a value"),
(&["--in", "--quiet"][..], "--in needs a value"),
(&["--out", "part.step"][..], "doesn't take --out"),
(&["--unknown"][..], "doesn't take --unknown"),
];
for command in ["eval", "inspect"] {
for existing in [false, true] {
let report = dir.0.join(format!("{command}-{existing}.json"));
if existing {
std::fs::write(&report, b"existing report").unwrap();
}
for (extra, message) in cases {
let output = cli().arg(command).arg(&script).arg("--json-out").arg(&report).args(extra).output().unwrap();
assert_eq!(output.status.code(), Some(1), "{command} {extra:?}");
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{command} {extra:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty());
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
if existing {
assert_eq!(std::fs::read(&report).unwrap(), b"existing report");
} else {
assert!(!report.exists());
}
}
}
}
}
#[test]
fn cli_loads_both_design_encodings_by_contents() {
let dir = Scratch::new();
+3
View File
@@ -22,6 +22,8 @@ solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
url = "2"
percent-encoding = "2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
@@ -34,5 +36,6 @@ web-sys = { version = "0.3", features = [
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
] }
+57 -7
View File
@@ -114,6 +114,46 @@ pub fn opens_design(extensions: &[&str]) -> bool {
extensions.is_empty() || extensions.iter().any(|e| solidcraft_engine::io::DESIGN_EXTS.contains(e))
}
/// Where an Open for `extensions` (what the command reads; `&[]`: anything) is answered.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OpenRoute {
/// The browser's open dialog for a design: the file keeps its handle, so Save writes back.
Disk,
/// The "Files in this browser" window, where the browser can't open a design from the disk.
Panel,
/// A plain file input that offers these types: the file is read into the engine's files and
/// the command runs on it (Insert DXF or SVG, images, meshes, imported settings).
Upload,
}
/// Choose how an Open for `extensions` asks for its file; `api`: the browser can open a file
/// from the disk and keep its handle.
pub fn open_route(api: bool, extensions: &[&str]) -> OpenRoute {
match (opens_design(extensions), api) {
(true, true) => OpenRoute::Disk,
(true, false) => OpenRoute::Panel,
(false, _) => OpenRoute::Upload,
}
}
/// The `accept` list of a file input for `extensions` (with or without their dot): ".dxf,.svg".
pub fn accept_list(extensions: &[&str]) -> String {
let list: Vec<String> = extensions
.iter()
.map(|e| e.trim().trim_start_matches('.'))
.filter(|e| !e.is_empty())
.map(|e| format!(".{}", e.to_ascii_lowercase()))
.collect();
list.join(",")
}
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
let image = |e: &&str| ["png", "jpg", "jpeg"].contains(&e.trim_start_matches('.').to_ascii_lowercase().as_str());
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
}
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
@@ -140,7 +180,7 @@ mod web {
use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::{FileSystemFileHandle, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransactionMode};
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, opens_design, parse, path, plan_prune, save_type};
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, parse, path, plan_prune, save_type};
use crate::files::{Files, err, read_handle};
const DB: &str = "solidcraft-disk-handles";
@@ -294,7 +334,7 @@ mod web {
let options = Object::new();
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
let extensions = Array::new();
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm"] {
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
extensions.push(&e.into());
}
let accept = Object::new();
@@ -315,11 +355,6 @@ mod web {
p
}
/// Does an Open for `extensions` use the browser's open dialog?
pub fn handles_open(extensions: &[&str]) -> bool {
supported() && opens_design(extensions)
}
// ---- writing ------------------------------------------------------------------------
/// Queue what a command wrote to a disk path. Writes go one at a time; a newer write of the
@@ -621,6 +656,21 @@ mod tests {
assert!(!opens_design(&["json"]));
}
/// Insert DXF or SVG, images and other non-design files get a file input that offers their
/// types in every browser (#2052); designs keep the disk dialog or the browser's files.
#[test]
fn opens_of_other_files_use_a_file_input_for_their_types() {
assert_eq!(open_route(true, &["dxf"]), OpenRoute::Upload);
assert_eq!(open_route(false, &["png", "jpg", "jpeg"]), OpenRoute::Upload);
assert_eq!(open_route(true, &[]), OpenRoute::Disk);
assert_eq!(open_route(false, &["solidcraft"]), OpenRoute::Panel);
assert_eq!(accept_list(&["dxf"]), ".dxf");
assert_eq!(accept_list(&["png", ".JPG", "", "jpeg"]), ".png,.jpg,.jpeg");
assert_eq!(accept_list(&[]), "");
assert!(upload_limit(&["png", "jpg", "jpeg"]).is_some());
assert_eq!(upload_limit(&["dxf"]), None);
}
#[test]
fn the_save_dialog_offers_the_type_of_the_name() {
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
+76 -2
View File
@@ -448,6 +448,18 @@ pub fn recovery_path() -> String {
format!("{OPFS}{RECOVERY}")
}
/// A recovery file lives in OPFS, but its relative links belong to the original design's
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
let path = recovery_path();
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
solidcraft_engine::resolve_design_links(&mut doc, original_path);
let bytes = solidcraft_engine::io::write_design_container(&doc).map_err(|e| e.to_string())?;
vfs::insert(&path, bytes);
Ok(path)
}
/// Pick files on the computer: the File System Access picker where there is one, else an
/// `<input type=file>`. Read files arrive in `files.incoming`.
pub fn pick_from_disk(files: &Rc<Files>, intent: Intent) {
@@ -500,8 +512,9 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
input.set_type("file");
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
// FreeCAD and Rhino files are part of it.
let accept = solidcraft_engine::io::formats::open_extensions(true).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
let accept =
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
input.set_accept(&accept);
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
let _ = input.set_attribute("style", "display:none");
@@ -527,6 +540,67 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
input.click();
}
/// Ask for one file of these types (`extensions`, e.g. `["dxf"]`) with a plain file input, the
/// dialog every browser has. The answer is the file's path in the engine's files (under
/// [`UPLOAD`], as for a file picked in the disk dialog), so the command that asked runs on it;
/// `None` when the dialog was closed or the file could not be read.
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
// Shown now, in the click: a file dialog needs the user's gesture.
let chosen = choose_file(&disk::accept_list(extensions));
let limit = disk::upload_limit(extensions);
let files = files.clone();
async move {
let read = async {
let value = JsFuture::from(chosen?).await.map_err(err)?;
// Closing the dialog answers no file: nothing to say.
let Ok(file) = value.dyn_into::<web_sys::File>() else { return Ok(None) };
let name = file.name();
if limit.is_some_and(|max| !file.size().is_finite() || file.size() > max as f64) {
return Err(format!("{name}: the file is too large"));
}
let bytes = blob_bytes(&file).await.map_err(|e| format!("{name}: {e}"))?;
let path = format!("{UPLOAD}{name}");
vfs::insert(&path, bytes);
Ok(Some(path))
};
match read.await {
Ok(path) => path,
Err(e) => {
files.say(format!("open: {e}"), true);
None
}
}
}
}
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
let doc = web_sys::window().and_then(|w| w.document()).ok_or("this page can't show a file dialog")?;
let input = doc.create_element("input").map_err(err)?.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| "no file input".to_string())?;
input.set_type("file");
input.set_accept(accept);
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
input.set_attribute("style", "display:none").map_err(err)?;
doc.body().ok_or("this page has no body")?.append_child(&input).map_err(err)?;
let promise = js_sys::Promise::new(&mut |resolve: Function, _reject: Function| {
// One answer for a pick or a cancel; read from the event, so it does not keep the input.
let done = Closure::once_into_js(move |event: web_sys::Event| {
let file = event.target().and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()).and_then(|input| {
input.set_onchange(None);
input.set_oncancel(None);
input.remove();
input.files().and_then(|list| list.get(0))
});
let _ = resolve.call1(&JsValue::UNDEFINED, &file.map_or(JsValue::NULL, JsValue::from));
});
input.set_onchange(Some(done.unchecked_ref()));
input.set_oncancel(Some(done.unchecked_ref()));
});
input.click();
Ok(promise)
}
/// Read files dropped on the page.
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
for f in std::mem::take(&mut raw.dropped_files) {
+137
View File
@@ -0,0 +1,137 @@
//! Bounded HTTPS or same-origin downloads, handed to the normal design-open command as unsaved copies.
use std::{cell::RefCell, rc::Rc};
use js_sys::{Reflect, Uint8Array};
use solidcraft_engine::io::vfs;
use solidcraft_ui_egui::{SolveApp, ui_kit};
use wasm_bindgen::{JsCast as _, JsValue};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use crate::{check_download_size, design_url};
type Reply = Result<(String, Vec<u8>), String>;
#[derive(Default)]
pub struct Download {
reply: Rc<RefCell<Option<Reply>>>,
pending: Option<(String, Vec<u8>)>,
}
impl Download {
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
let download = Self::default();
if let Some(url) = url {
let url = url.to_owned();
let slot = download.reply.clone();
let ctx = ctx.clone();
spawn_local(async move {
*slot.borrow_mut() = Some(fetch(&url).await);
ctx.request_repaint();
});
}
download
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
if let Some(reply) = self.reply.borrow_mut().take() {
match reply {
Ok(file) => self.pending = Some(file),
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
}
}
if self.pending.is_none() || app.file_picker_busy() {
return;
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
ui.horizontal(|ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
answer = Some(true);
}
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
answer = Some(false);
}
});
});
match answer {
Some(true) => {}
Some(false) => {
self.pending = None;
return;
}
None if closed => {
self.pending = None;
return;
}
None => return,
}
}
let Some((name, bytes)) = self.pending.take() else { return };
// The extension selects the design loader even when the URL has no extension.
let path = "/linked/design.solidcraft";
vfs::insert(path, bytes);
app.open_path(path);
if app.session.path.as_deref() == Some(path) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
} else {
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
}
let _ = vfs::remove(path);
}
}
async fn fetch(value: &str) -> Reply {
let window = web_sys::window().ok_or("No browser window")?;
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
let url = design_url(value, &page_origin)?;
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
if !response.ok() {
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
}
let length = response
.headers()
.get("Content-Length")
.map_err(|_| "Cannot read download size")?
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
.transpose()?;
if let Some(length) = length {
check_download_size(0, length)?;
}
let stream = response.body().ok_or("Download has no body")?;
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
let result = read(&reader).await;
if result.is_err() {
let _ = JsFuture::from(reader.cancel()).await;
}
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
// can describe compressed bytes, so the streamed limit also checks the decoded size.
let bytes = result?;
Ok((name, bytes))
}
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
let mut bytes = Vec::new();
// Bound even a stream which keeps returning empty chunks.
for _ in 0..1_000_000 {
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
return Ok(bytes);
}
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
check_download_size(bytes.len(), u64::from(chunk.length()))?;
bytes.extend_from_slice(&chunk.to_vec());
}
Err("Download returned too many chunks".into())
}
+246 -24
View File
@@ -14,6 +14,8 @@ mod disk;
mod files;
#[cfg(target_arch = "wasm32")]
mod install;
#[cfg(target_arch = "wasm32")]
mod linked;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
@@ -25,6 +27,89 @@ fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
ctrl && key == "Tab"
}
#[cfg(any(target_arch = "wasm32", test))]
fn query_flag(query: &str, name: &str) -> bool {
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
}
/// Launch options use exact query keys; URL values are decoded only once.
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Default, Debug)]
struct Launch {
open: Option<String>,
embed: bool,
}
#[cfg(any(target_arch = "wasm32", test))]
impl Launch {
fn parse(query: &str) -> Self {
let mut launch = Self::default();
let mut embed = false;
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
match key.as_ref() {
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
"embed" => embed = true,
_ => {}
}
}
launch.embed = embed && launch.open.is_some();
launch
}
fn persists(&self) -> bool {
!self.embed
}
fn full_app_url(&self, mut url: url::Url) -> String {
url.set_query(None);
url.set_fragment(None);
if let Some(open) = &self.open {
url.query_pairs_mut().append_pair("open", open);
}
url.to_string()
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn design_url(value: &str, page_origin: &str) -> Result<url::Url, String> {
if value.len() > 16_384 {
return Err("Design URL is too long".into());
}
let decoded = percent_encoding::percent_decode_str(value).decode_utf8().map_err(|_| "Invalid design URL encoding")?;
if decoded.chars().any(|ch| ch.is_control() || ch == '\\') {
return Err("Design links must not contain backslashes or control characters".into());
}
// Check before URL parsing normalizes away dot segments. Decode escaped slashes too.
let path = value.split(['?', '#']).next().unwrap_or_default();
let path = percent_encoding::percent_decode_str(path).collect::<Vec<_>>();
if path.split(|byte| *byte == b'/').any(|segment| segment == b"..") {
return Err("Design links must not contain parent path segments".into());
}
let page = url::Url::parse(page_origin).map_err(|_| "Invalid page origin")?;
let url = if value.starts_with('/') && !value.starts_with("//") {
page.join(value).map_err(|_| "Invalid design path")?
} else {
url::Url::parse(value).map_err(|_| "Invalid design URL")?
};
if (url.scheme() != "https" && url.origin() != page.origin())
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
{
return Err("Design links must use HTTPS or the page's own origin without credentials".into());
}
Ok(url)
}
#[cfg(any(target_arch = "wasm32", test))]
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
@@ -112,6 +197,9 @@ mod web {
struct Shell {
app: SolveApp,
launch: crate::Launch,
linked: crate::linked::Download,
full_url: Option<String>,
api: Api,
files: Rc<Files>,
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
@@ -137,6 +225,9 @@ mod web {
impl Shell {
/// Files from the computer, writes to store, messages and the autosave.
fn files_tick(&mut self, ctx: &egui::Context) {
if !self.launch.persists() {
return;
}
if self.files.ctx.borrow().is_none() {
*self.files.ctx.borrow_mut() = Some(ctx.clone());
}
@@ -267,7 +358,7 @@ mod web {
files::pick_from_disk(&self.files, Intent::Open);
}
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
.on_hover_text("STEP, 3MF or STL into the open design")
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
.clicked()
{
files::pick_from_disk(&self.files, Intent::Insert);
@@ -320,7 +411,7 @@ mod web {
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.on_hover_text("Into the open design (STEP, 3MF, STL)")
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
.clicked()
{
act = Some(("insert", e.name.clone()));
@@ -435,15 +526,25 @@ mod web {
});
});
let Some(recover) = answer else { return };
*self.files.recovery.borrow_mut() = None;
if recover {
self.app.open_path(&files::recovery_path());
let recovery = match files::prepare_recovery(path.as_deref()) {
Ok(recovery) => recovery,
Err(error) => {
self.app.set_status(error, true);
return;
}
};
self.app.open_path(&recovery);
if self.app.session.path.as_deref() != Some(recovery.as_str()) {
return;
}
// It belongs to its own file again (saving goes there); it is unsaved.
self.app.session.path = path;
self.app.session.doc_mut().name = name;
self.app.session.mark_unsaved();
self.app.home.open = false;
}
*self.files.recovery.borrow_mut() = None;
files::autosave(&self.files, None);
}
@@ -526,19 +627,42 @@ mod web {
if let Some(delay) = self.wheel_gestures.update(raw, now) {
ctx.request_repaint_after(delay);
}
files::take_drops(&self.files, raw);
if self.launch.persists() {
files::take_drops(&self.files, raw);
} else {
raw.dropped_files.clear();
}
// Before the app's own hook, which adds the control channel's events.
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
self.app.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.linked.tick(&mut self.app, ui.ctx());
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
if self.launch.embed {
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
{
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
}
});
}
self.app.ui(ui);
let ctx = ui.ctx().clone();
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
if self.launch.persists() {
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
}
self.watch_text_for_font(&ctx);
}
fn persist_egui_memory(&self) -> bool {
self.launch.persists()
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.launch.persists() {
return;
}
// Persist the user's own language, not a session-only English fallback.
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
@@ -557,15 +681,15 @@ mod web {
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
fn webgl() -> bool {
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
let q = query();
if q.contains("webgl") {
if crate::query_flag(&q, "webgl") {
if let Some(s) = &storage {
let _ = s.set_item(WEBGL_KEY, "1");
}
return true;
}
if q.contains("webgpu") {
if crate::query_flag(&q, "webgpu") {
if let Some(s) = &storage {
let _ = s.remove_item(WEBGL_KEY);
}
@@ -591,7 +715,10 @@ mod web {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
ask_before_unload(&window);
let launch = crate::Launch::parse(&query());
if launch.persists() {
ask_before_unload(&window);
}
leave_tab_switching_to_browser(&window);
let Some(document) = window.document() else { return };
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
@@ -611,19 +738,24 @@ mod web {
Box::new(move |cc| {
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
let files = Rc::new(Files::default());
files::start(&files);
if launch.persists() {
files::start(&files);
}
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
let (restorable, restorer) = (files.clone(), files.clone());
let services = Services {
let services = if launch.embed { Services::default() } else { Services {
// Open shows the browser's open dialog for a design where it has one
// (the file keeps its handle, so Save writes back to it); otherwise,
// and for inserting STEP or meshes, the browser's files window.
pick_open: Some(Box::new(move |exts: &[&str]| {
if disk::handles_open(exts) {
return Box::pin(disk::pick_open(&opener));
// (the file keeps its handle, so Save writes back to it), else the
// browser's files window. A command that reads another kind of file
// (Insert DXF or SVG, an image, a mesh, settings) gets a file dialog
// offering that kind, and runs on the picked file.
pick_open: Some(Box::new(move |exts: &[&str]| match disk::open_route(disk::supported(), exts) {
disk::OpenRoute::Disk => Box::pin(disk::pick_open(&opener)),
disk::OpenRoute::Upload => Box::pin(files::pick_upload(&opener, exts)),
disk::OpenRoute::Panel => {
panel.panel.set(true);
Box::pin(std::future::ready(None))
}
panel.panel.set(true);
Box::pin(std::future::ready(None))
})),
// A design is saved to a file the user picks, or kept in the browser
// where the browser can't pick one; everything else is downloaded.
@@ -648,26 +780,27 @@ mod web {
browser_files: Some(Box::new(move || shown.panel.set(true))),
install_app: Some(crate::install::service(&cc.egui_ctx)),
..Default::default()
};
}};
// Thumbnails of saves and start-page cards.
solidcraft_ui_egui::gpu::install_cpu_picture_renderer();
let (sender, receiver) = std::sync::mpsc::channel();
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.embedded = launch.embed;
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
// eframe's web painter has no stencil buffer.
app.set_wgpu(rs, DEPTH_BITS, 0, 1);
}
if query().contains("sample") {
if launch.open.is_none() && crate::query_flag(&query(), "sample") {
if let Err(e) = app.session.run_script(&solidcraft_engine::sample::script()) {
app.set_status(format!("sample: {e}"), true);
}
app.session.doc_mut().name = "Sample Plate".into();
app.session.mark_saved();
}
if !query().contains("sample") {
if launch.open.is_none() && !crate::query_flag(&query(), "sample") {
// The empty design takes Units of new designs (#1205). The sample
// part is built in millimetres and is left that way.
solidcraft_ui_egui::prefs::apply_new_design(&mut app);
@@ -676,8 +809,13 @@ mod web {
}
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
let font_language = app.preferences.language.clone();
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|url| launch.full_app_url(url));
Ok(Box::new(Shell {
app,
launch,
linked,
full_url,
api,
files,
autosaved_at: f64::NEG_INFINITY,
@@ -721,6 +859,90 @@ fn main() {
mod tests {
use super::*;
#[test]
fn link_query_decodes_once_and_embed_disables_persistence() {
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
assert!(query_flag("?embed&webgl", "webgl"));
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
assert!(launch.embed);
assert!(!launch.persists());
assert!(!Launch::parse("?embed").embed);
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
assert!(Launch::parse("?open=https://example.org/a").persists());
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
let launch = Launch::parse("?embed&open=/dutchman/jobs/abc/part.solidcraft%3Fx%3Da%2520b");
assert!(launch.embed);
assert!(!launch.persists());
let page = url::Url::parse("http://solidcraft.io/?embed#view").expect("synthetic page");
let full = url::Url::parse(&launch.full_app_url(page)).expect("full app URL");
let reopened = Launch::parse(full.query().unwrap_or_default());
assert_eq!(reopened.open, launch.open);
assert_eq!(reopened.open.as_deref(), Some("/dutchman/jobs/abc/part.solidcraft?x=a%20b"));
assert!(!reopened.embed);
assert!(reopened.persists());
assert!(full.fragment().is_none());
}
#[test]
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
for value in [
"http://example.org/a",
"data:application/json,{}",
"blob:https://example.org/a",
"javascript:alert(1)",
"https://user:secret@example.org/a",
"//evil.example/x",
"/../x",
"/%2e%2e/x",
"/a/%2E./x",
"/a%2f..%2fx",
"/a\\x",
"/a%5cx",
"/a\nx",
"/a%00x",
"/a%7fx",
"/a%C2%85x",
"/a%3f/../x",
"/a/..#view",
"/a/..?download=1",
"https://solidcraft.io/a/../x",
"http://user:secret@solidcraft.io/x",
"http://solidcraft.io.evil.example/x",
"http://solidcraft.io:8080/x",
"relative.solidcraft",
"",
] {
assert!(design_url(value, "http://solidcraft.io").is_err(), "{value}");
}
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384)), "http://solidcraft.io").is_err());
assert!(design_url("http://redirect.example.org/a", "http://solidcraft.io").is_err());
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
assert!(check_download_size(0, cap as u64).is_ok());
assert!(check_download_size(0, cap as u64 + 1).is_err());
assert!(check_download_size(cap - 2, 2).is_ok());
assert!(check_download_size(cap - 2, 3).is_err());
assert!(check_download_size(0, u64::MAX).is_err());
assert!(check_download_size(cap, 1).is_err());
assert!(check_download_size(usize::MAX, 0).is_err());
}
#[test]
fn design_links_allow_same_origin_paths_and_redirects() {
for origin in ["http://solidcraft.io", "https://solidcraft.io", "http://solidcraft.io:8080"] {
assert_eq!(design_url("/a/b.solidcraft", origin).map(|url| url.to_string()), Ok(format!("{origin}/a/b.solidcraft")));
assert!(design_url(&format!("{origin}/x"), origin).is_ok());
// Final response URLs follow the same origin and scheme rules as the request.
assert!(design_url("https://docs.solidcraft.io/x", origin).is_ok());
assert!(design_url("http://other/x", origin).is_err());
}
assert!(design_url("http://solidcraft.io/x", "https://solidcraft.io").is_err());
assert!(design_url("http://SOLIDCRAFT.io:80/x", "http://solidcraft.io").is_ok());
}
#[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.
+246 -8
View File
@@ -13,11 +13,15 @@ use solidcraft_engine::MAX_LINE;
use solidcraft_engine::control_auth;
use solidcraft_engine::control_pending;
use solidcraft_ui_egui::ControlRequest;
use solidcraft_ui_egui::control::{DESIGN_WATCH, DesignWatch, Subscriber};
/// Connections served at once, so a flood must not be able to spawn threads without end.
const MAX_CONNECTIONS: usize = 16;
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
const MAX_BAD_LINES: usize = 8;
/// How long a subscribed connection waits after a `design.changed` line before the next: what
/// changes meanwhile (a parameter dragged) goes out as one line, the latest (#6985).
const EVENT_GAP: Duration = Duration::from_millis(100);
static OPEN: AtomicUsize = AtomicUsize::new(0);
/// The token files this launch wrote (its control port, the agent chat's endpoint), removed
@@ -127,7 +131,7 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
OPEN.fetch_sub(1, Ordering::Relaxed);
let mut refused = stream;
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
continue;
}
let tx = tx.clone();
@@ -174,6 +178,15 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
}
}
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
/// of its pieces apart, a system call for every string, number and comma: half a second for a
/// design's component tree when it has thousands of components.
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
line.push(b'\n');
out.write_all(&line)
}
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
@@ -184,7 +197,7 @@ fn unauthorized() -> Value {
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
use std::io::Read;
let _ = writeln!(out, "{}", unauthorized());
let _ = send(out, &unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
@@ -356,10 +369,91 @@ fn busy_message(p: &Pending, method: &str) -> String {
)
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING);
/// A subscribed connection's socket, for its replies and its events: each line is written whole
/// and alone, so an event never lands inside a reply (#6985).
struct Lines {
socket: TcpStream,
turn: Mutex<()>,
}
impl Lines {
fn send(&self, line: &Value) -> std::io::Result<()> {
let _turn = self.turn.lock().unwrap_or_else(PoisonError::into_inner);
send(&mut &self.socket, line)
}
}
/// A connection's `control.subscribe` (#6985): its `design.changed` lines, sent from a thread of
/// their own until the connection ends.
struct Subscription {
lines: Arc<Lines>,
stop: Arc<AtomicBool>,
_counted: Subscriber,
}
impl Subscription {
/// Write `reply`, the window's answer to `control.subscribe` naming the design `seen`, on
/// `out`, then send `design`'s changes after it: the reply goes out before any event. `None`,
/// with nothing written, when no thread or socket was left for the events.
fn start(
design: &'static DesignWatch,
counted: Subscriber,
out: &TcpStream,
seen: (String, u64),
reply: &Value,
) -> Option<(Self, std::io::Result<()>)> {
let lines = Arc::new(Lines { socket: out.try_clone().ok()?, turn: Mutex::new(()) });
let stop = Arc::new(AtomicBool::new(false));
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
let (l, s) = (lines.clone(), stop.clone());
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
let sent = send(&mut &lines.socket, reply);
drop(first);
Some((Subscription { lines, stop, _counted: counted }, sent))
}
}
/// The design a `control.subscribe` reply names.
fn design_of(reply: &Value) -> Option<(String, u64)> {
let result = reply.get("result")?;
Some((result.get("document_id")?.as_str()?.to_string(), result.get("revision")?.as_u64()?))
}
impl Drop for Subscription {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
// The socket closes now, not when the thread next looks; a line blocked on a client that
// stopped reading ends too.
let _ = self.lines.socket.shutdown(std::net::Shutdown::Both);
}
}
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
/// last one it was told of (`seen`), until `stop`.
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
while !stop.load(Ordering::Relaxed) {
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
break;
}
seen = now;
std::thread::sleep(EVENT_GAP);
}
}
/// Write `reply` on the connection: through its subscription once it has one.
fn answer(out: &mut TcpStream, events: Option<&Subscription>, reply: &Value) -> std::io::Result<()> {
match events {
Some(e) => e.lines.send(reply),
None => send(out, reply),
}
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING, &DESIGN_WATCH);
}
#[allow(clippy::too_many_arguments)]
fn serve_with(
stream: TcpStream,
token: &str,
@@ -368,6 +462,7 @@ fn serve_with(
patient: bool,
waits: Waits,
running: &'static Running,
design: &'static DesignWatch,
) {
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
let Ok(read) = stream.try_clone() else { return };
@@ -376,12 +471,15 @@ fn serve_with(
let mut bad = 0usize;
let mut authenticated = false;
let mut pending: Option<Pending> = None;
let mut events: Option<Subscription> = None;
loop {
// A first `control.subscribe` on its way to the window (#6985).
let mut joining = None;
let line = match read_line(&mut reader, MAX_LINE) {
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
Ok(None) => break,
Err(e) => {
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
break;
}
};
@@ -425,6 +523,14 @@ fn serve_with(
} else if let Some(p) = pending.as_ref().filter(|_| late.is_none()) {
// Nothing more is queued behind it, to run unseen when it ends.
json!({"ok": false, "busy": true, "error": busy_message(p, &method)})
} else if method == "control.subscribe" && events.is_some() {
if late.is_some() {
// As below: finished, and the client moved on without asking.
pending = None;
}
// Its events already say how the design moves; a second answer could only
// arrive behind a newer one.
json!({"ok": false, "error": "already subscribed: this connection is sent design.changed lines; ask document.revision for the design now"})
} else {
if late.is_some() {
// Finished, and the client moved on without asking: it is dropped.
@@ -434,6 +540,9 @@ fn serve_with(
let wait = if patient || method == "engine.batch" { waits.long } else { waits.short };
let what = control_pending::describe(&method, &params);
let effect = control_pending::effect(&method, &params);
// Counted before the window answers, so it publishes every change from its
// answer on.
joining = (method == "control.subscribe").then(|| design.join());
let (req, rrx) = ControlRequest::new(method.clone(), params);
let ticket = running.add(connection, &id, &req);
if tx.send(req).is_err() {
@@ -463,7 +572,20 @@ fn serve_with(
json!({"ok": false, "error": format!("bad JSON: {e}")})
}
};
if writeln!(out, "{reply}").is_err() {
// Subscribed once the window named the design (not when it refused or timed out).
let sent = match joining.and_then(|counted| design_of(&reply).map(|seen| (counted, seen))) {
Some((counted, seen)) => match Subscription::start(design, counted, &out, seen, &reply) {
Some((subscription, sent)) => {
events = Some(subscription);
sent
}
None => {
send(&mut out, &json!({"id": reply.get("id"), "ok": false, "error": "not subscribed: no thread or socket left for the events"}))
}
},
None => answer(&mut out, events.as_ref(), &reply),
};
if sent.is_err() {
break;
}
if bad >= MAX_BAD_LINES {
@@ -503,7 +625,7 @@ mod tests {
static RUNNING: Running = Running::new();
std::thread::spawn(move || {
if let Some(stream) = listener.incoming().flatten().next() {
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING);
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH);
}
});
let s = TcpStream::connect(addr).unwrap();
@@ -569,7 +691,7 @@ mod tests {
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING));
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH));
}
});
let open = || {
@@ -639,6 +761,95 @@ mod tests {
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(7), &json!(true)), "{r}");
}
/// #6985: a connection that sends `control.subscribe` gets the design in the reply, then a
/// `design.changed` line each time the window publishes another: never before the reply,
/// none for the same design, the latest after a burst, each line whole between its replies.
/// A second subscribe is refused without reaching the window. A connection that did not
/// subscribe gets no events, and one that closed is no longer counted.
#[test]
fn subscribed_connections_are_told_of_design_changes() {
static RUNNING: Running = Running::new();
static WATCH: DesignWatch = DesignWatch::new();
let token = "0123456789abcdef0123456789abcdef";
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
// The window: it answers `control.subscribe` as the app does, publishing the design first;
// then the design changes at once, before the reply may have been written.
std::thread::spawn(move || {
for req in rx {
if req.method == "control.subscribe" {
WATCH.publish("d-1", 3);
let _ = req.reply.send(json!({"ok": true, "result": {"document_id": "d-1", "revision": 3}}));
WATCH.publish("d-1", 4);
} else {
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
}
}
});
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, WAITS, &RUNNING, &WATCH));
}
});
let open = || {
let s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
(s.try_clone().unwrap(), BufReader::new(s))
};
let next = |lines: &mut BufReader<TcpStream>| -> Value {
let mut l = String::new();
lines.read_line(&mut l).unwrap();
serde_json::from_str(&l).unwrap()
};
let changed = |id: &str, revision: u64| json!({"event": "design.changed", "params": {"document_id": id, "revision": revision}});
let (mut quiet_out, mut quiet) = open();
writeln!(quiet_out, "{}", json!({"id": 1, "method": "ui.view", "token": token})).unwrap();
assert_eq!(next(&mut quiet)["id"], 1);
let (mut out, mut lines) = open();
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
assert_eq!(next(&mut lines), json!({"id": 1, "ok": true, "result": {"document_id": "d-1", "revision": 3}}));
assert_eq!(next(&mut lines), changed("d-1", 4), "the change after the window's answer comes after the reply");
assert!(WATCH.subscribed());
// The design it was told of is no news; another revision is.
WATCH.publish("d-1", 4);
WATCH.publish("d-1", 5);
assert_eq!(next(&mut lines), changed("d-1", 5));
// A burst ends with its latest.
for revision in 6..=10 {
WATCH.publish("d-1", revision);
}
let mut last = 5;
while last < 10 {
let e = next(&mut lines);
let revision = e["params"]["revision"].as_u64().unwrap();
assert!(e["event"] == "design.changed" && revision > last, "{e} after {last}");
last = revision;
}
// Another tab: another id, whatever its revision.
WATCH.publish("d-2", 2);
assert_eq!(next(&mut lines), changed("d-2", 2));
// Subscribed already: refused here (the window would have published d-1 again).
writeln!(out, "{}", json!({"id": 2, "method": "control.subscribe"})).unwrap();
let r = next(&mut lines);
assert!(r["id"] == 2 && r["ok"] == false && r["error"].as_str().is_some_and(|e| e.starts_with("already subscribed")), "{r}");
// Replies still come, whole, and nothing else was sent meanwhile.
writeln!(out, "{}", json!({"id": 3, "method": "ui.view"})).unwrap();
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
// Closed, it is no longer counted.
drop((out, lines));
let deadline = Instant::now() + Duration::from_secs(10);
while WATCH.subscribed() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(!WATCH.subscribed());
}
/// A timed-out request that only reads is not said to "still apply", nor is one that only
/// writes a file (an export, #1989); a change is, by name.
#[test]
@@ -750,4 +961,31 @@ mod tests {
let mut r = BufReader::with_capacity(64, text.as_slice());
assert!(read_line(&mut r, 1000).is_err());
}
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
/// thousands of components took half a second to send), and reads back as it was.
#[test]
fn a_large_reply_is_written_at_once() {
struct Counted {
writes: usize,
bytes: Vec<u8>,
}
impl Write for Counted {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writes += 1;
self.bytes.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
let mut out = Counted { writes: 0, bytes: Vec::new() };
send(&mut out, &reply).unwrap();
assert_eq!(out.writes, 1);
assert_eq!(out.bytes.last(), Some(&b'\n'));
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
}
}
+141 -23
View File
@@ -28,6 +28,7 @@ mod native_crash;
mod native_frame;
#[cfg(target_os = "macos")]
mod native_menu;
mod nav_bridge;
#[cfg(target_os = "macos")]
mod open_documents;
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
@@ -87,6 +88,8 @@ struct App {
space_mouse: space_mouse::SpaceMouse,
#[cfg(target_os = "macos")]
native_menu: Option<native_menu::NativeMenu>,
/// A device's own app moving the view (Preferences › Navigation, off by default).
nav_bridge: nav_bridge::NavBridge,
}
impl eframe::App for App {
@@ -145,6 +148,8 @@ impl eframe::App for App {
if !self.solve.file_picker_busy() {
self.space_mouse.tick(&mut self.solve, ctx);
}
// Every frame, so switching the bridge off reaches the reader while a picker is open too.
self.nav_bridge.tick(&mut self.solve, ctx);
if self.solve.quit_requested {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
@@ -182,6 +187,10 @@ impl eframe::App for App {
}
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
// Before a restart below starts the next instance, which would otherwise get nothing from
// the 3D mouse driver (`solidcraft_3dx_client::stop`).
#[cfg(target_os = "macos")]
solidcraft_3dx_client::stop();
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);
@@ -292,7 +301,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
dialog = dialog.add_filter(label, &case_variants(first));
}
// 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)));
let mut everything = io::formats::open_extensions(true);
everything.extend(["dxf", "svg"]);
dialog = dialog.add_filter(solidcraft_ui_egui::i18n::tr("file.open.drawing_filter"), &case_variants(&everything));
for (label, exts) in io::formats::import_filters(true) {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
@@ -335,6 +346,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
let future = dialog.save_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
graphics_adapters: Vec::new(),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
window_system: window_system_now(),
http_get: Some(std::sync::Arc::new(http_get)),
@@ -575,13 +587,22 @@ fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]
}
}
/// Anti-aliasing samples from Preferences (4 when unset).
fn msaa() -> u16 {
/// Keep the default until the installed renderer's startup check proves a fallback is needed.
fn scene_msaa_request(saved: Option<u16>) -> u16 {
saved.unwrap_or(MSAA)
}
/// Query the scene's colour/depth intersection, rather than the depthless window's counts.
fn scene_sample_choices(depth_bits: u8, stencil_bits: u8, query: impl FnOnce(Option<eframe::wgpu::TextureFormat>) -> Vec<u32>) -> Vec<u32> {
query(eframe::egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits))
}
/// An explicitly saved anti-aliasing choice, absent until Graphics changes it.
fn msaa() -> Option<u16> {
graphics()
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
.and_then(|m| u16::try_from(m).ok())
.filter(|m| [1, 2, 4, 8].contains(m))
.unwrap_or(MSAA)
}
/// Windows and Linux: no OS title bar; the application bar is the title bar
@@ -621,6 +642,18 @@ fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::
/// The window's app id: it names the desktop entry, so it is the packages' reverse-DNS id.
const APP_ID: &str = solidcraft_engine::io::APP_ID;
/// Disconnects from the 3D mouse driver when dropped, as `main` returns or a panic unwinds out
/// of it (`solidcraft_3dx_client::stop`).
#[cfg(target_os = "macos")]
struct DriverClient;
#[cfg(target_os = "macos")]
impl Drop for DriverClient {
fn drop(&mut self) {
solidcraft_3dx_client::stop();
}
}
fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
@@ -685,7 +718,13 @@ fn main() -> eframe::Result {
if safe_mode {
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
}
let samples = if safe_mode { MSAA } else { msaa() };
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
if let Some(note) = &fallback_note {
log::warn!("{note}");
eprintln!("SolidCraft: {note}");
notes.push(note.clone());
}
let saved_samples = if safe_mode { None } else { msaa() };
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
@@ -763,6 +802,10 @@ fn main() -> eframe::Result {
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let created_in_app = created.clone();
let starting = settings.clone();
// A panic on the window's thread ends the app without `on_exit`; this disconnects from the
// 3D mouse driver then too.
#[cfg(target_os = "macos")]
let _driver_client = DriverClient;
mark("run_native");
let result = eframe::run_native(
"SolidCraft",
@@ -793,6 +836,7 @@ fn main() -> eframe::Result {
}
app.integrated_titlebar = cfg!(target_os = "macos");
app.cpu_meshing = safe_mode;
app.graphics_fallback = fallback_note.clone();
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
@@ -802,19 +846,37 @@ fn main() -> eframe::Result {
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
lost_at: None,
});
if !safe_mode {
app.preferences.graphics_adapter =
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
}
if let Some(rs) = &cc.wgpu_render_state {
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
app.services.graphics_adapters = rs
.available_adapters
.iter()
.map(|adapter| {
let info = adapter.get_info();
(info.name.clone(), format!("{} ({:?}, {:?})", info.name, info.device_type, info.backend))
})
.collect();
let info = rs.adapter.get_info();
let samples = scene_msaa_request(saved_samples);
let supported = scene_sample_choices(depth_bits, STENCIL_BITS, |depth| {
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
});
let scene_samples = scene_samples(info.backend, u32::from(samples), &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_split(rs, depth_bits, STENCIL_BITS, scene_samples);
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
app.msaa_supported = supported;
app.preferences.msaa = u32::from(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.
#[cfg(target_os = "windows")]
native_crash::reinstall();
// A saved anti-aliasing the device can't do: back to the default, with a notice.
// Reflect a confirmed probe fallback or an unsupported saved count in Graphics.
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 {scene_samples}x",
info.backend, info.name, info.driver
@@ -876,8 +938,9 @@ fn main() -> eframe::Result {
if !notes.is_empty() {
app.set_status(notes.join(" "), true);
}
let nav_bridge = nav_bridge::NavBridge::start(&cc.egui_ctx);
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx, nav_bridge.claim());
#[cfg(target_os = "macos")]
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
Ok(menu) => Some(menu),
@@ -902,12 +965,13 @@ fn main() -> eframe::Result {
space_mouse,
#[cfg(target_os = "macos")]
native_menu,
nav_bridge,
}))
}),
);
control_server::remove_token_file();
match result {
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, settings.as_deref()),
r => r,
}
}
@@ -928,6 +992,9 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
// A retry applies only to this launch. WGPU_BACKEND always has priority.
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
eprintln!("SolidCraft graphics: starting {backends:?}");
setup.instance_descriptor.backends = backends;
@@ -935,9 +1002,9 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
}
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
startup::configure_adapter(setup, cpu, environment, adapter);
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
let base = setup.device_descriptor.clone();
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
@@ -949,24 +1016,47 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
options
}
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
/// the software renderer, else a message box saying what failed instead of a silent exit.
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
/// A failed renderer returns here before app creation. A fresh process avoids reusing winit's
/// event loop or a failed device; each retry advances through the APIs, then software once.
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
log::error!("the window could not open: {error}");
eprintln!("SolidCraft: the window could not open: {error}");
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
// This failure was handled, so neither the marker nor the journal should call it a crash.
if let Some(dir) = settings {
startup::started(dir);
}
solidcraft_engine::crash::end_session();
let explicit = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
if let Some(next) = startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed)
&& let Ok(exe) = std::env::current_exe()
{
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
log::warn!("{note}");
eprintln!("SolidCraft: {note}");
let mut command = std::process::Command::new(exe);
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
if next == "cpu" {
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
} else {
command.env(startup::RETRY_BACKEND_ENV, next);
}
match command.status() {
Ok(status) if status.success() => return Ok(()),
// The second try showed its own message.
// The final try showed its own message.
Ok(_) => return Err(error),
Err(e) => log::error!("could not start again with the software renderer: {e}"),
Err(e) => log::error!("could not start again on {next}: {e}"),
}
}
let message = if explicit {
format!("SolidCraft could not open its window with WGPU_BACKEND.\n\n{error}\n\nChange or remove WGPU_BACKEND to try another graphics API.")
} else {
startup::no_window_message(&error.to_string(), settings)
};
let _ = rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title("SolidCraft could not start")
.set_description(startup::no_window_message(&error.to_string(), settings))
.set_description(message)
.set_buttons(rfd::MessageButtons::Ok)
.show();
Err(error)
@@ -1187,6 +1277,34 @@ mod msaa_tests {
}
}
#[test]
fn scene_sample_choices_include_the_scene_depth_format() {
use eframe::wgpu::TextureFormat;
for (bits, expected) in [(24, TextureFormat::Depth24PlusStencil8), (32, TextureFormat::Depth32FloatStencil8)] {
// A synthetic colour target supports 8x, but the scene depth supports only 1x/4x.
let supported = super::scene_sample_choices(bits, 8, |depth| match depth {
Some(format) if format == expected => vec![1, 4],
None => vec![1, 4, 8],
other => panic!("unexpected scene depth: {other:?}"),
});
assert_eq!(supported, [1, 4]);
assert_eq!(super::scene_samples(eframe::wgpu::Backend::Dx12, 8, &supported), 4);
}
}
#[test]
fn scene_msaa_keeps_four_samples_by_default_and_explicit_choices() {
use eframe::wgpu::Backend;
let default = super::scene_msaa_request(None);
assert_eq!(default, 4);
for backend in [Backend::Dx12, Backend::Vulkan, Backend::Metal] {
assert_eq!(super::scene_samples(backend, u32::from(default), &[1, 4]), 4);
}
for saved in [1, 2, 4, 8] {
assert_eq!(super::scene_msaa_request(Some(saved)), saved);
}
}
#[test]
fn the_scene_follows_the_backend_policy_too() {
use eframe::wgpu::Backend;
+463
View File
@@ -0,0 +1,463 @@
//! The navigation bridge: a 3D input device's own program (a homemade device's driver app) moves
//! the view over a loopback connection, the way a 3D mouse driver hands its axes to the programs
//! it supports, so the device emulates no mouse and the pointer stays the user's.
//!
//! The device app listens on `127.0.0.1:<port>`; with Preferences › Navigation › "Navigate with a
//! device app" on (off by default), SolidCraft connects to it, says hello and reads one JSON
//! object per line (docs/3d-mouse.md). The reader thread only sums the motion; the egui thread
//! hands it to the view once per frame (`space_mouse::bridge_frame`). A device the app says it
//! reads is left to it by the 3D mouse reader ([`Claim`]), so the view doesn't move twice.
use std::io::{ErrorKind, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use egui::Context;
use serde_json::{Value, json};
use solidcraft_ui_egui::SolveApp;
use solidcraft_ui_egui::space_mouse::{BRIDGE_VIEWS, BridgeMotion, bridge_frame, bridge_step};
/// The longest line read; a longer one is skipped to its end.
const MAX_LINE: usize = 4096;
/// Standard views waiting for the next frame; past this many, more are a flood and dropped.
const MAX_VIEWS: usize = 8;
/// The longest name a device app may give for itself or its device, in characters.
const MAX_NAME: usize = 64;
/// How long a connection attempt waits.
const CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
/// How long a read waits for data before the reader looks at the preferences again.
const READ_WAKE: Duration = Duration::from_millis(250);
/// The wait before connecting again when no device app answered or one went away.
const RETRY_AFTER: Duration = Duration::from_secs(1);
/// In zoom mode, a motion of this many points along Y is one zoom step: with 8 % a step, the 1 %
/// a point that a zoom drag moves.
const POINTS_PER_ZOOM_STEP: f64 = 8.0;
/// What the device's motion drives, as the device app last said (`mode`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum Mode {
#[default]
Orbit,
Pan,
Zoom,
}
/// What the reader thread and the egui thread share.
#[derive(Debug, Default)]
struct Shared {
/// The port the preferences ask to connect to; none while the bridge is off.
wanted: Option<u16>,
/// The device app connected, by the name it gave ("?" until it gives one).
connected: Option<String>,
/// The device the connected app reads, as it named it: the 3D mouse reader leaves that one alone.
device: Option<String>,
/// Motion since the egui thread last took it.
motion: BridgeMotion,
/// Standard views asked for since then.
views: Vec<String>,
}
fn lock(shared: &Mutex<Shared>) -> Option<MutexGuard<'_, Shared>> {
shared.lock().ok()
}
/// A name a device app gave, without control characters and cut to [`MAX_NAME`]; none when empty.
fn name(value: Option<&Value>) -> Option<String> {
let name: String = value?.as_str()?.chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
let name = name.trim();
(!name.is_empty()).then(|| name.to_string())
}
/// A number of a message, finite and in range; none when it isn't one.
fn number(msg: &Value, key: &str) -> f64 {
bridge_step(msg.get(key).and_then(Value::as_f64).unwrap_or(0.0))
}
/// Takes one line from the device app into `shared`. Lines that aren't a message this version
/// knows are ignored. Returns whether the view has something new to show.
fn consume(line: &[u8], mode: &mut Mode, shared: &mut Shared) -> bool {
let Ok(msg) = serde_json::from_slice::<Value>(line) else { return false };
let add = |shared: &mut Shared, motion: BridgeMotion| {
shared.motion = shared.motion.plus(motion);
!motion.is_still()
};
match msg.get("t").and_then(Value::as_str) {
Some("axis") => {
let (x, y) = (number(&msg, "x"), number(&msg, "y"));
let motion = match mode {
Mode::Orbit => BridgeMotion { orbit: [x, y], ..Default::default() },
Mode::Pan => BridgeMotion { pan: [x, y], ..Default::default() },
// Up zooms in, as a zoom drag does.
Mode::Zoom => BridgeMotion { zoom: -y / POINTS_PER_ZOOM_STEP, ..Default::default() },
};
add(shared, motion)
}
Some("zoom") => add(shared, BridgeMotion { zoom: number(&msg, "d"), ..Default::default() }),
Some("mode") => {
let on = msg.get("on").and_then(Value::as_bool).unwrap_or(false);
*mode = match msg.get("mode").and_then(Value::as_str) {
Some("pan") if on => Mode::Pan,
Some("zoom") if on => Mode::Zoom,
_ => Mode::Orbit,
};
false
}
Some("view") => {
let view = msg.get("name").and_then(Value::as_str).and_then(|n| BRIDGE_VIEWS.iter().find(|v| **v == n));
match view {
Some(view) if shared.views.len() < MAX_VIEWS => {
shared.views.push((*view).to_string());
true
}
_ => false,
}
}
Some("hello") => {
shared.connected = name(msg.get("server")).or_else(|| shared.connected.clone());
shared.device = name(msg.get("device"));
true
}
_ => false,
}
}
/// Splits what arrives into lines, across reads. A line longer than [`MAX_LINE`] is dropped up to
/// its newline, so a peer that never sends one can't make the reader hold more than that.
#[derive(Default)]
struct Lines {
line: Vec<u8>,
skipping: bool,
}
impl Lines {
fn feed(&mut self, mut data: &[u8], mut each: impl FnMut(&[u8])) {
while !data.is_empty() {
let end = data.iter().position(|b| *b == b'\n');
let (part, rest) = match end {
Some(i) => (data.get(..i).unwrap_or_default(), data.get(i + 1..).unwrap_or_default()),
None => (data, &[][..]),
};
if !self.skipping {
let room = (MAX_LINE + 1).saturating_sub(self.line.len());
self.line.extend_from_slice(part.get(..room.min(part.len())).unwrap_or_default());
if self.line.len() > MAX_LINE {
self.line.clear();
self.skipping = true;
}
}
if end.is_some() {
if !self.skipping {
each(self.line.strip_suffix(b"\r").unwrap_or(&self.line));
}
self.line.clear();
self.skipping = false;
}
data = rest;
}
}
}
/// What SolidCraft says first, so the device app knows which program it is driving.
fn hello() -> Value {
json!({"t": "hello", "app": "solidcraft", "version": env!("CARGO_PKG_VERSION")})
}
/// Reads one connection until the device app closes it, it fails, or the preferences no longer
/// ask for this port; then forgets what it had.
fn serve(mut stream: TcpStream, port: u16, ctx: &Context, shared: &Mutex<Shared>) {
// Without a read timeout the reader couldn't notice the bridge being switched off.
if stream.set_read_timeout(Some(READ_WAKE)).is_err() || writeln!(stream, "{}", hello()).is_err() {
return;
}
let _ = stream.set_nodelay(true);
log::info!("navigation bridge: connected to 127.0.0.1:{port}");
if let Some(mut shared) = lock(shared) {
shared.connected = Some("?".into());
}
ctx.request_repaint();
let (mut lines, mut mode) = (Lines::default(), Mode::default());
let mut chunk = [0_u8; 4096];
while lock(shared).is_some_and(|shared| shared.wanted == Some(port)) {
match stream.read(&mut chunk) {
Ok(0) => break,
Ok(size) => {
let mut news = false;
if let Some(mut shared) = lock(shared) {
lines.feed(chunk.get(..size).unwrap_or_default(), |line| news |= consume(line, &mut mode, &mut shared));
}
if news {
ctx.request_repaint();
}
}
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted) => {}
Err(error) => {
log::info!("navigation bridge: connection lost: {error}");
break;
}
}
}
if let Some(mut shared) = lock(shared) {
*shared = Shared { wanted: shared.wanted, ..Shared::default() };
}
ctx.request_repaint();
}
/// Connects to the device app while the preferences ask for it, again whenever it goes away.
fn reader(ctx: Context, shared: Arc<Mutex<Shared>>) {
loop {
let Some(port) = lock(&shared).and_then(|shared| shared.wanted) else {
std::thread::sleep(READ_WAKE);
continue;
};
if let Ok(stream) = TcpStream::connect_timeout(&SocketAddr::from((Ipv4Addr::LOCALHOST, port)), CONNECT_TIMEOUT) {
serve(stream, port, &ctx, &shared);
}
std::thread::sleep(RETRY_AFTER);
}
}
/// Which 3D mouse the connected device app reads, for the 3D mouse reader to leave alone. Only
/// the Windows and macOS readers know a device's strings to compare.
#[derive(Clone)]
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
pub struct Claim(Arc<Mutex<Shared>>);
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
impl Claim {
/// Whether a device with these manufacturer and product strings is the one the connected
/// device app reads (its name appears in one, ignoring case).
pub fn claims(&self, strings: &[String]) -> bool {
let Some(device) = lock(&self.0).and_then(|shared| shared.device.clone()) else { return false };
let device = device.to_lowercase();
strings.iter().any(|s| s.to_lowercase().contains(&device))
}
}
pub struct NavBridge {
shared: Arc<Mutex<Shared>>,
}
impl NavBridge {
pub fn start(ctx: &Context) -> Self {
let shared = Arc::new(Mutex::new(Shared::default()));
let (worker_ctx, worker_state) = (ctx.clone(), Arc::clone(&shared));
if let Err(error) = std::thread::Builder::new().name("solidcraft-nav-bridge".into()).spawn(move || reader(worker_ctx, worker_state)) {
log::warn!("navigation bridge: reader could not start: {error}");
}
Self { shared }
}
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
pub fn claim(&self) -> Claim {
Claim(Arc::clone(&self.shared))
}
/// Each frame: passes the preferences to the reader, and the motion and views since the last
/// frame to the view. Motion that arrived while the window had no focus is dropped with it.
pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) {
let wanted = Some(app.preferences.nav_bridge_port).filter(|port| app.preferences.nav_bridge_enabled && *port != 0);
let Some((motion, views, connected)) = lock(&self.shared).map(|mut shared| {
shared.wanted = wanted;
(std::mem::take(&mut shared.motion), std::mem::take(&mut shared.views), shared.connected.clone())
}) else {
return;
};
app.space_mouse.bridge = connected.filter(|_| wanted.is_some());
if !motion.is_still() || !views.is_empty() {
bridge_frame(app, ctx, motion, &views);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufRead, BufReader};
use std::net::TcpListener;
use std::time::Instant;
fn feed(shared: &mut Shared, mode: &mut Mode, text: &str) -> bool {
let mut news = false;
Lines::default().feed(text.as_bytes(), |line| news |= consume(line, mode, shared));
news
}
#[test]
fn axis_motion_orbits_pans_or_zooms_as_the_mode_says() {
let (mut shared, mut mode) = (Shared::default(), Mode::default());
assert!(feed(&mut shared, &mut mode, "{\"t\":\"axis\",\"x\":3,\"y\":-2}\n{\"t\":\"axis\",\"x\":1.5,\"y\":0}\n"));
assert_eq!(shared.motion, BridgeMotion { orbit: [4.5, -2.0], ..Default::default() });
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":true}\n{\"t\":\"axis\",\"x\":5,\"y\":6}\n");
assert_eq!(mode, Mode::Pan);
assert_eq!(shared.motion.pan, [5.0, 6.0]);
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"zoom\",\"on\":true}\n{\"t\":\"axis\",\"x\":0,\"y\":-16}\n");
assert_eq!(shared.motion.zoom, 2.0, "up zooms in, 8 points a step");
// Off, whichever mode it names, is back to orbit.
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":false}\n");
assert_eq!(mode, Mode::Orbit);
// The ring's steps add up with the zoom mode's.
feed(&mut shared, &mut mode, "{\"t\":\"zoom\",\"d\":-0.5}\n");
assert_eq!(shared.motion.zoom, 1.5);
}
#[test]
fn views_are_queued_by_their_known_names_only() {
let (mut shared, mut mode) = (Shared::default(), Mode::default());
assert!(feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"iso\"}\n{\"t\":\"view\",\"name\":\"fit\"}\n"));
assert!(!feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"ISO\"}\n{\"t\":\"view\",\"name\":\"../etc\"}\n{\"t\":\"view\"}\n"));
assert_eq!(shared.views, ["iso", "fit"]);
let flood = "{\"t\":\"view\",\"name\":\"top\"}\n".repeat(100);
feed(&mut shared, &mut mode, &flood);
assert_eq!(shared.views.len(), MAX_VIEWS);
}
#[test]
fn hostile_lines_never_move_the_view_out_of_range() {
let (mut shared, mut mode) = (Shared::default(), Mode::default());
let lines = [
"not json",
"{\"t\":\"axis\",\"x\":\"5\",\"y\":null}",
"{\"t\":\"axis\",\"x\":1e308,\"y\":-1e308}",
"{\"t\":\"axis\",\"x\":1e999}",
"{\"t\":\"zoom\",\"d\":[1]}",
"{\"t\":42}",
"[1,2,3]",
"{\"t\":\"mode\",\"mode\":{\"x\":1},\"on\":\"yes\"}",
"\u{0}\u{1}\u{2}",
];
for line in lines {
Lines::default().feed(format!("{line}\n").as_bytes(), |l| {
consume(l, &mut mode, &mut shared);
});
}
let limit = solidcraft_ui_egui::space_mouse::BRIDGE_MAX_STEP;
let values = shared.motion.orbit.into_iter().chain(shared.motion.pan).chain([shared.motion.zoom]);
for value in values {
assert!(value.is_finite() && value.abs() <= limit, "{value}");
}
assert_eq!(mode, Mode::Orbit);
// Invalid UTF-8 is no message either.
assert!(!consume(&[0xff, 0xfe, b'{', b'}'], &mut mode, &mut shared));
}
#[test]
fn lines_split_across_reads_and_long_ones_are_skipped_to_their_end() {
let mut got = Vec::new();
let mut lines = Lines::default();
for part in ["{\"t\":", "\"zoom\"}\r\n{\"a\"", ":1}\n"] {
lines.feed(part.as_bytes(), |line| got.push(String::from_utf8_lossy(line).to_string()));
}
assert_eq!(got, ["{\"t\":\"zoom\"}", "{\"a\":1}"]);
got.clear();
let long = vec![b'x'; MAX_LINE * 3];
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
assert!(lines.line.len() <= MAX_LINE + 1, "never holds more than one line");
lines.feed(b"tail\nnext\n", |line| got.push(String::from_utf8_lossy(line).to_string()));
assert_eq!(got, ["next"], "the long line is dropped to its newline, the next one read");
}
#[test]
fn the_device_app_names_itself_and_the_device_it_leaves_alone() {
let shared = Arc::new(Mutex::new(Shared::default()));
let claim = Claim(Arc::clone(&shared));
let strings = ["STMicroelectronics".to_string(), "MAKMOUSE 3D".to_string()];
assert!(!claim.claims(&strings), "nothing is claimed before an app says so");
let mut mode = Mode::default();
if let Ok(mut shared) = shared.lock() {
let hello = format!("{{\"t\":\"hello\",\"server\":\"Dev\\u0007App {}\",\"device\":\"makmouse\"}}\n", "x".repeat(200));
feed(&mut shared, &mut mode, &hello);
assert_eq!(shared.connected.as_deref().map(|n| n.chars().count()), Some(MAX_NAME));
assert!(shared.connected.as_deref().is_some_and(|n| n.starts_with("DevApp")));
}
assert!(claim.claims(&strings), "the name matches ignoring case");
assert!(!claim.claims(&["3Dconnexion".to_string(), "SpaceMouse Compact".to_string()]), "another 3D mouse stays the reader's");
}
/// Waits until `check` holds, or fails after a few seconds.
fn wait_for(what: &str, mut check: impl FnMut() -> bool) {
let deadline = Instant::now() + Duration::from_secs(5);
while !check() {
assert!(Instant::now() < deadline, "timed out waiting for {what}");
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn a_connection_says_hello_reads_the_stream_and_ends_when_the_bridge_is_switched_off() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
let (mut server, _) = listener.accept().unwrap();
let worker = {
let shared = Arc::clone(&shared);
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
};
let mut hello_line = String::new();
BufReader::new(server.try_clone().unwrap()).read_line(&mut hello_line).unwrap();
let hello: Value = serde_json::from_str(&hello_line).unwrap();
assert_eq!((hello["t"].as_str(), hello["app"].as_str()), (Some("hello"), Some("solidcraft")));
server
.write_all(b"{\"t\":\"hello\",\"server\":\"MAKMOUSE\",\"device\":\"MAKMOUSE\"}\n{\"t\":\"axis\",\"x\":2,\"y\":1}\n{\"t\":\"view\",\"name\":\"top\"}\n")
.unwrap();
wait_for("the stream", || shared.lock().is_ok_and(|s| s.motion.orbit == [2.0, 1.0] && s.views == ["top"]));
assert_eq!(shared.lock().unwrap().connected.as_deref(), Some("MAKMOUSE"));
// Switched off: the connection ends and what it had is forgotten.
shared.lock().unwrap().wanted = None;
worker.join().unwrap();
let after = shared.lock().unwrap();
assert!(after.connected.is_none() && after.device.is_none() && after.motion.is_still() && after.views.is_empty());
}
#[test]
fn a_closed_connection_forgets_the_app() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
let (server, _) = listener.accept().unwrap();
let worker = {
let shared = Arc::clone(&shared);
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
};
wait_for("the connection", || shared.lock().is_ok_and(|s| s.connected.is_some()));
drop(server);
worker.join().unwrap();
let after = shared.lock().unwrap();
assert_eq!((after.connected.as_deref(), after.wanted), (None, Some(port)), "still wanted, so the reader connects again");
}
#[test]
fn each_frame_hands_the_view_the_motion_and_passes_the_preferences_on() {
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
app.home.open = false;
let ctx = Context::default();
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
let queue = |bridge: &NavBridge| {
let mut shared = bridge.shared.lock().unwrap();
shared.connected = Some("MAKMOUSE".into());
shared.motion = BridgeMotion { orbit: [10.0, 0.0], ..Default::default() };
};
// Off (the default): nothing is wanted, nothing reaches the view.
queue(&bridge);
bridge.tick(&mut app, &ctx);
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
assert!(app.viewport.bridge_motion.is_none() && app.space_mouse.bridge.is_none());
// On: the port is passed to the reader and the motion to the view, once.
app.preferences.nav_bridge_enabled = true;
app.preferences.nav_bridge_port = 9123;
queue(&bridge);
bridge.tick(&mut app, &ctx);
assert_eq!(bridge.shared.lock().unwrap().wanted, Some(9123));
assert_eq!(app.space_mouse.bridge.as_deref(), Some("MAKMOUSE"));
assert_eq!(app.viewport.bridge_motion.map(|m| m.orbit), Some([10.0, 0.0]));
assert!(bridge.shared.lock().unwrap().motion.is_still(), "taken, not handed over twice");
// Port 0 is no port.
app.preferences.nav_bridge_port = 0;
bridge.tick(&mut app, &ctx);
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
}
}
+26 -4
View File
@@ -528,6 +528,9 @@ struct Candidate<D> {
device: D,
layout: Layout,
ids: (u16, u16),
/// The device's manufacturer and product strings ([`device_strings`]).
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
strings: Vec<String>,
}
/// Input reports read with a timeout in milliseconds: hidapi's devices, and the tests' simulated
@@ -634,8 +637,15 @@ fn follow<D: ReportSource>(mut candidates: Vec<Candidate<D>>, now: impl Fn() ->
}
}
/// The device's manufacturer and product strings, which say whether a device app has it
/// ([`crate::nav_bridge::Claim`]). Asked of the opened 3D mouse only.
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
fn device_strings(device: &hidapi::HidDevice) -> Vec<String> {
[device.get_manufacturer_string(), device.get_product_string()].into_iter().filter_map(|s| s.ok().flatten()).collect()
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>, bridge: crate::nav_bridge::Claim) {
let Ok(mut api) = hid_api() else {
log::warn!("3D mouse: HID initialization failed");
return;
@@ -657,11 +667,17 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
// 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(Candidate { device, layout, ids: (info.vendor_id(), info.product_id()) })
let strings = device_strings(&device);
Some(Candidate { device, layout, ids: (info.vendor_id(), info.product_id()), strings })
})
.collect();
let names: Vec<_> = open.iter().map(|candidate| (candidate.ids, candidate.strings.clone())).collect();
// Whether the device read now is one a device app has: it moves the view over the navigation
// bridge, so read here as well the device would move it twice. What was read before ages out.
let mut strings: &[String] = &[];
follow(open, Instant::now, |event| match event {
Followed::Device(ids, buttons) => {
strings = names.iter().find(|(known, _)| *known == ids).map_or(&[][..], |(_, strings)| strings.as_slice());
connected(&ctx, &shared, Source::Hid(Some(ids)));
// Taken over from another device: what it reported meanwhile is its baseline.
if let Some(buttons) = buttons
@@ -670,6 +686,7 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
reading.set_buttons(buttons);
}
}
Followed::Report(..) if bridge.claims(strings) => {}
Followed::Report(layout, report) => publish(&ctx, &shared, *layout, report),
Followed::Gone => reset(&ctx, &shared),
});
@@ -1054,7 +1071,8 @@ pub struct SpaceMouse {
}
impl SpaceMouse {
pub fn start(ctx: &Context) -> Self {
/// Starts the reader. A device that `bridge` says its own app reads is left to that app.
pub fn start(ctx: &Context, bridge: crate::nav_bridge::Claim) -> Self {
let now = Instant::now();
let shared = Arc::new(Mutex::new(Reading::new(now)));
let worker_state = Arc::clone(&shared);
@@ -1063,11 +1081,14 @@ impl SpaceMouse {
let drivers = Arc::new(Drivers::default());
#[cfg(target_os = "linux")]
let worker_drivers = Arc::clone(&drivers);
// hidraw names no device's strings to compare (`Claim`).
#[cfg(target_os = "linux")]
let _ = bridge;
let spawned = std::thread::Builder::new().name("solidcraft-3d-mouse".into()).spawn(move || {
#[cfg(target_os = "linux")]
reader(worker_ctx, worker_state, worker_drivers, Machine);
#[cfg(not(target_os = "linux"))]
reader(worker_ctx, worker_state);
reader(worker_ctx, worker_state, bridge);
});
if let Err(error) = spawned {
log::warn!("3D mouse: reader could not start: {error}");
@@ -1458,6 +1479,7 @@ mod tests {
},
layout,
ids: (0x256f, *product),
strings: Vec::new(),
})
.collect();
let (mut seen, mut read) = (Vec::new(), 0);
+151 -2
View File
@@ -8,8 +8,9 @@
//! row the next launch starts in safe mode by itself.
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
//! - **No usable graphics device**: one retry on the software renderer
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
//! - **A renderer that cannot start**: on Windows, try the remaining graphics APIs in order,
//! then the software renderer (`SOLIDCRAFT_RENDERER=cpu`). An explicit `WGPU_BACKEND` fails
//! cleanly without switching. Exhausted retries show a message box with the error.
use std::path::{Path, PathBuf};
@@ -129,6 +130,48 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
})
}
/// Adapter properties used to choose a card without opening a device.
#[derive(Clone, Debug)]
pub struct AdapterChoice {
pub name: String,
pub compatible: bool,
}
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
/// environment override follows wgpu's case-insensitive substring convention.
/// No match leaves selection to the normal wgpu request.
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
if let Some(name) = environment.filter(|s| !s.is_empty()) {
let name = name.to_lowercase();
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
return Some(index);
}
}
if let Some(name) = preference.filter(|s| !s.is_empty())
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
{
return Some(index);
}
None
}
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
let choices: Vec<_> = adapters
.iter()
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
.collect();
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
}));
}
}
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
@@ -176,6 +219,34 @@ pub fn choose_backends(
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
}
/// Internal, process-local retry choice; never written to the graphics preference.
pub const RETRY_BACKEND_ENV: &str = "SOLIDCRAFT_STARTUP_BACKEND";
/// Why this launch is using a fallback, carried to the new process for status and Preferences.
pub const FALLBACK_NOTE_ENV: &str = "SOLIDCRAFT_GRAPHICS_FALLBACK";
/// A handled startup failure tries each remaining Windows API once, then software rendering.
/// An explicit environment choice is final, including when software rendering was requested.
pub fn fallback_backend(windows: bool, explicit: bool, cpu: bool, failed: eframe::wgpu::Backends) -> Option<&'static str> {
if explicit || cpu {
return None;
}
if windows {
let last = WINDOWS_ORDER.iter().rposition(|backend| failed.intersects(*backend));
if let Some(next) = last.and_then(|i| WINDOWS_ORDER.get(i + 1)) {
return Some(if *next == eframe::wgpu::Backends::VULKAN { "vulkan" } else { "gl" });
}
}
Some("cpu")
}
/// Keep driver diagnostics on one bounded line, including across the finite retry chain.
pub fn fallback_note(previous: &str, failed: eframe::wgpu::Backends, next: &str, error: &str) -> String {
let previous: String = previous.chars().take(2048).collect();
let error: String = error.chars().take(1024).collect();
let line = format!("{previous} Graphics fallback: {failed:?} failed ({error}); trying {next}.");
line.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(4096).collect()
}
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
@@ -249,6 +320,48 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
mod tests {
use super::*;
fn cards() -> Vec<AdapterChoice> {
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
}
#[test]
fn adapter_names_environment_override_and_missing_card_fallback() {
let cards = cards();
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
}
#[test]
fn automatic_adapter_leaves_selection_to_wgpu() {
let mut cards = cards();
assert_eq!(choose_adapter(&cards, None, None), None);
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
cards[2].compatible = false;
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
assert_eq!(choose_adapter(&[], None, None), None);
}
#[test]
fn automatic_adapter_preserves_the_previous_request_configuration() {
use eframe::egui_wgpu::WgpuSetupCreateNew;
let previous = WgpuSetupCreateNew::without_display_handle();
for preference in [None, Some(String::new())] {
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, None, preference);
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
assert_eq!(setup.power_preference, previous.power_preference);
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
}
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
}
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
@@ -358,6 +471,42 @@ mod tests {
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
}
#[test]
fn failed_windows_backends_fall_back_in_order_and_stop_after_software() {
use eframe::wgpu::Backends as B;
assert_eq!(fallback_backend(true, false, false, B::DX12), Some("vulkan"));
assert_eq!(fallback_backend(true, false, false, B::VULKAN), Some("gl"));
assert_eq!(fallback_backend(true, false, false, B::GL), Some("cpu"));
assert_eq!(fallback_backend(true, false, true, B::DX12), None);
assert_eq!(fallback_backend(true, false, false, B::DX12 | B::VULKAN), Some("gl"));
assert_eq!(fallback_backend(false, false, false, B::VULKAN), Some("cpu"));
let note = fallback_note("", B::DX12, "vulkan", "egui_pipeline\n0x80070057");
assert!(note.contains("egui_pipeline 0x80070057") && note.contains("vulkan") && !note.contains('\n'));
let long = fallback_note(&"x".repeat(5000), B::VULKAN, "gl", &"y".repeat(5000));
assert!(long.chars().count() <= 4096 && long.ends_with("trying gl."));
}
#[test]
fn explicit_backend_failure_never_switches_api_or_retries_software() {
use eframe::wgpu::Backends as B;
for windows in [false, true] {
for cpu in [false, true] {
for backend in [B::DX12, B::VULKAN, B::GL, B::all()] {
assert_eq!(fallback_backend(windows, true, cpu, backend), None);
let mut probed = false;
assert_eq!(
choose_backends(windows, true, cpu, Some("vulkan"), |_| {
probed = true;
false
}),
None
);
assert!(!probed);
}
}
}
}
#[test]
fn only_opengl_on_windows_gets_the_system_title_bar() {
use eframe::wgpu::Backends;
+1
View File
@@ -25,6 +25,7 @@ assets/
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
locales/{en,de,ja,ko}.json # shared offline catalogs
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
archive/icons/macos-v1/
source/icon.vectorcraft # earlier editable work, retained
# redundant historical exports are retired
+32
View File
@@ -48,6 +48,38 @@ source = "#236ead"
light = "#236ead"
dark = "#2976b8"
# Simultaneous component colours for inspection icons; existing workspace ramps stay unchanged.
[roles.component-orange-1]
source = "#f1d2af"
light = "#f1d2af"
dark = "#ba8f6a"
[roles.component-orange-2]
source = "#c06f00"
light = "#c06f00"
dark = "#b86f04"
[roles.component-orange-3]
source = "#a45000"
light = "#a45000"
dark = "#95682d"
[roles.component-yellow-1]
source = "#e5d99b"
light = "#e5d99b"
dark = "#a9985f"
[roles.component-yellow-2]
source = "#a28200"
light = "#a28200"
dark = "#9a8100"
[roles.component-yellow-3]
source = "#826700"
light = "#826700"
dark = "#857100"
[strokes] # on the 64 grid
geometry = 2.55
action = 4.25
@@ -57,6 +57,8 @@ circle_tt false
circle_ttt false
close false
coil false
colored_components false
colored_components_off false
combine false
compare false
component false
@@ -245,6 +247,7 @@ status_error false
status_recovery false
style false
suppress false
surface false
surface_blend false
surface_boundary false
surface_extend false
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Colored components</title>
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#c06f00"/>
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#a45000"/>
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#f1d2af"/>
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#a28200"/>
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#826700"/>
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#e5d99b"/>
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Disable component colors</title>
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+3 -3
View File
@@ -152,11 +152,11 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
pub(crate) const AXIS_RADIUS: f32 = 15.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
let col = if d.x != 0.0 {
t.triad[0]
t.axis_x
} else if d.y != 0.0 {
t.triad[1]
t.axis_y
} else {
t.triad[2]
t.axis_z
};
let positive = !name.starts_with('-');
let light = t.panel.r() > 128;
+8 -1
View File
@@ -270,7 +270,7 @@
"Preview Row": "Náhled řádku",
"Preview leaves the current design unchanged.": "Náhled aktuální návrh nemění.",
"Profiles/Curves": "Profily/křivky",
"Quantity": "Množství",
"Quantity": "Počet",
"RECENT": "NEDÁVNÉ",
"REFERENCE": "REFERENCE",
"RULED SURFACE": "PŘÍMKOVÁ PLOCHA",
@@ -974,6 +974,7 @@
"motion.key_here": "Vložit klíčový snímek sem",
"motion.key_here_hint": "Přidat v tomto kroku klíčový snímek s aktuální hodnotou kloubu",
"motion.no_collisions": "Během studie nedochází ke kolizím",
"motion.plot_quantity": "Množství",
"motion.step_back": "Krok zpět",
"motion.step_forward": "Krok vpřed",
"motion.to_end": "Na konec",
@@ -1556,6 +1557,7 @@
"ui.ai_http_busy": "endpoint je zaneprázdněný nebo překročil limit; zkuste to později",
"ui.ai_http_key": "endpoint odmítl klíč: zkontrolujte jej v Předvolby > AI",
"ui.ai_http_not_found": "takový endpoint nebo model neexistuje: zkontrolujte základní URL a název modelu",
"ui.ai_http_too_large": "konverzace je pro tento endpoint příliš velká; začněte nový chat",
"ui.ai_key": "API klíč",
"ui.ai_key_hint": "volitelné; uloženo v systémové klíčence",
"ui.ai_key_needs_https": "Klíč se odesílá pouze přes HTTPS nebo na tento počítač.",
@@ -1759,6 +1761,8 @@
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
"ui.findInTimeline": "Najít na časové ose",
"ui.findInWindow": "Najít v okně",
"ui.fine": "Jemná",
"ui.finest": "Nejjemnější",
"ui.finish_sketch": "Dokončit náčrt",
"ui.fit": "Přizpůsobit",
"ui.fix_unfix": "Zafixovat/uvolnit",
@@ -1848,6 +1852,7 @@
"ui.heading_torus": "ANULOID",
"ui.heading_utilities": "NÁSTROJE",
"ui.heading_utility": "NÁSTROJE",
"ui.heading_weldments": "SVAŘENCE",
"ui.help": "Nápověda",
"ui.hide": "Skrýt",
"ui.hideAxes": "Skrýt osy",
@@ -1986,6 +1991,7 @@
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
"ui.properties": "Vlastnosti",
"ui.pull_direction": "Směr vyjímání",
"ui.quality": "Kvalita",
"ui.quantity": "Počet",
"ui.quantity_2": "Počet 2",
"ui.quit": "Ukončit",
@@ -2080,6 +2086,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Rozdělit těleso",
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
"ui.standard": "Standardní",
"ui.start": "Start",
"ui.start_over": "Začít znovu",
"ui.start_page": "Úvodní stránka",
+392 -6
View File
@@ -5,7 +5,9 @@
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Vedhæft dit skærmbillede: Kopiér og indsæt det, eller gem og upload PNG-filen.",
"2D": "2D",
"3. Click Submit new issue on GitHub to send your report.": "3. Klik på Submit new issue på GitHub for at sende rapporten.",
"A Single hole snaps to the middle of its face and to the centres of its round edges": "Et enkelt hul snapper til fladens midtpunkt og centrene af dens cirkulære kanter",
"A new configuration, a copy of the active one": "Opret en ny konfiguration som en kopi af den aktive",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "Et placeret enkelt hul har et håndtag til at trække i. Hvis fladen vælges igen, flyttes hullet dertil i stedet for at blive fjernet",
"ARRANGE": "ARRANGER",
"About SolidCraft": "Om SolidCraft",
"Acceleration": "Acceleration",
@@ -14,23 +16,31 @@
"Active": "Aktiv",
"Active rule": "Aktiv regel",
"Add": "Tilføj",
"Add Column": "Tilføj kolonne",
"Add Key": "Tilføj animationspunkt",
"Add Mid Radius": "Tilføj mellemradius",
"Add Row": "Tilføj række",
"Add a tool": "Tilføj et værktøj",
"Add condition": "Tilføj betingelse",
"Add or edit rule": "Tilføj eller redigér regel",
"Add to New Folder": "Føj til ny mappe",
"Advanced": "Avanceret",
"Against": "Mod",
"Alignment": "Justering",
"All Lines": "Alle linjer",
"All Sketch Points": "Alle skitsepunkter",
"All components": "Alle komponenter",
"All open designs": "Alle åbne designs",
"Allowed expressions, separated by semicolons": "Tilladte udtryk adskilt af semikolon",
"Also deletes": "Sletter også",
"Anchor": "Forankring",
"Angle 2": "Vinkel 2",
"Animate": "Animér",
"App/system information and command settings are included. Review private details before sharing.": "Oplysninger om programmet, systemet og kommandoindstillingerne er inkluderet. Gennemgå private oplysninger, før du deler rapporten.",
"Appearance name": "Navn på udseende",
"Applied Radius": "Anvendt radius",
"Apply To": "Anvend på",
"Apply library appearance": "Anvend udseende fra biblioteket",
"Apply to selection": "Anvend på markering",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Anvend først dit udseende. Eksporterer tildelte udseender og materialer oprettet i dette design.",
"Arc length": "Buelængde",
@@ -41,13 +51,22 @@
"BLEND SURFACE": "OVERGANGSFLADE",
"BOUNDARY SURFACE": "AFGRÆNSET OVERFLADE",
"BROWSER": "MODELTRÆ",
"Bar": "Stang",
"Bend Edges": "Bøjningskanter",
"Bend Line Position": "Bukkelinjens placering",
"Bend Position": "Bukningens placering",
"Bend Radius": "Bukkeradius",
"Bend Table": "Bukketabel",
"Bends": "Bukninger",
"Bodies": "Emner",
"Bodies (optional)": "Legemer (valgfrit)",
"Body": "Emne",
"Body (last if none)": "Legeme (det sidste, hvis intet er valgt)",
"Body to split": "Legeme, der skal opdeles",
"Body, or faces to take out": "Legeme eller flader, der skal fjernes",
"Bottom Left": "Nederst til venstre",
"Bottom Middle": "Nederst i midten",
"Bottom Right": "Nederst til højre",
"Boundary": "Afgrænsning",
"Bounding box": "Omsluttende boks",
"CHECK": "KONTROLLÉR",
@@ -59,32 +78,45 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV eller JSON: Opdaterer eksisterende parametre og tilføjer nye",
"Capture All Positions": "Registrér alle positioner",
"Capture Pos.": "Registrér pos.",
"Catch Depth": "Låsedybde",
"Catch Direction": "Indgrebsretning",
"Catch Length": "Låselængde",
"Cells": "Celler",
"Center X": "Centrum X",
"Center Y": "Centrum Y",
"Center of mass": "Massecentrum",
"Centre to centre": "Centrum til centrum",
"Centroid": "Tyngdepunkt",
"Chamfer Type": "Affasningstype",
"Channel": "U-profil",
"Check collisions": "Kontrollér kollisioner",
"Checked": "Kontrolleret",
"Choose Variable radius or Spline in Type.": "Vælg variabel radius eller spline under Type.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Vælg et nyt plan. Skitsegeometri og mål bevarer deres todimensionelle koordinater.",
"Choose…": "Vælg…",
"Circle center": "Cirklens centrum",
"Circular": "Cirkulær",
"Circumference": "Omkreds",
"Clockwise": "Med uret",
"Close draft": "Luk kladde",
"Collapse": "Fold sammen",
"Collisions": "Kollisioner",
"Colour": "Farve",
"Command": "Kommando",
"Comment": "Kommentar",
"Component 1": "Komponent 1",
"Component 2": "Komponent 2",
"Component1": "Komponent1",
"Components": "Komponenter",
"Config Table": "Konfig.tabel",
"Configuration": "Konfiguration",
"Configuration rules": "Konfigurationsregler",
"Contact": "Kontakt",
"Contact Sets": "Kontaktsæt",
"Contact sets": "Kontaktsæt",
"Continue on GitHub": "Fortsæt på GitHub",
"Continuity": "Kontinuitet",
"Control Radius": "Styreradius",
"Copy Error Message": "Kopiér fejlmeddelelse",
"Copy Warning Message": "Kopiér advarselsmeddelelse",
"Copy all licences": "Kopiér alle licenser",
@@ -93,9 +125,14 @@
"Copy screenshot": "Kopiér skærmbillede",
"Corner Relief": "Hjørneaflastning",
"Corner Relief Shape": "Hjørneaflastningens form",
"Corner radius": "Hjørneradius",
"Counterbore depth": "Dybde af cylindrisk forsænkning",
"Counterbore Ø": "Cylindrisk forsænkning Ø",
"Countersink angle": "Vinkel på konisk forsænkning",
"Countersink Ø": "Konisk forsænkning Ø",
"Create Hole": "Opret hul",
"Create Issue — review draft": "Opret issue — gennemgå kladde",
"Create Issue…": "Opret fejlrapport…",
"Cross curves": "Tværkurver",
"Cube": "Terning",
"Current orthographic view (1:1 mm)": "Aktuel visning uden perspektiv (1:1 mm)",
@@ -103,22 +140,33 @@
"Curv. Comb": "Krumningskam",
"Curvature (G2)": "Krumningskontinuitet (G2)",
"Curvature change (G3)": "Ændring i krumning (G3)",
"Custom": "Brugerdefineret",
"Customize Toolbar": "Tilpas værktøjslinje",
"Customize…": "Tilpas…",
"Cylindrical face": "Cylindrisk flade",
"Delete": "Slet",
"Delete Selected Features…": "Slet valgte features…",
"Delete Selected Folders": "Slet valgte mapper",
"Delete all": "Slet alle",
"Delete rule": "Slet regel",
"Delete the row": "Slet rækken",
"Delete the set": "Slet sættet",
"Density": "Densitet",
"Dependents": "Afhængige",
"Describe the problem you saw": "Beskriv problemet, du oplevede",
"Describe what happened before continuing.": "Beskriv, hvad der skete, før du fortsætter.",
"Designs you open or save appear here.": "Designs, du åbner eller gemmer, vises her.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestemmes af Til objekt. Vælg Vinkel for at ændre vinkeludstrækningen.",
"Diameter": "Diameter",
"Direction": "Retning",
"Discard": "Kassér",
"Distance": "Afstand",
"Distance 1": "Afstand 1",
"Do not export hidden edges": "Eksportér ikke skjulte kanter",
"Don't ask again": "Spørg ikke igen",
"Double-click to rename": "Dobbeltklik for at omdøbe",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Dobbeltklik på den midterste museknap i visningen: Hjem (hjemmevisningen, der indrammer det viste), Tilpas (indrammer det viste og bevarer orienteringen) eller ingen handling",
"Drag to move the hole; click to remove it": "Træk for at flytte hullet; klik for at fjerne det",
"Drive Joint": "Bevæg led",
"Duration": "Varighed",
"Edges": "Kanter",
@@ -126,80 +174,113 @@
"Edit Profile Sketch": "Redigér profilskitse",
"Enable rule": "Aktivér regel",
"Enabled": "Aktiveret",
"End Radius": "Slutradius",
"End bulge": "Udbuling ved slutningen",
"End continuity": "Overgang ved slutningen",
"Expand": "Fold ud",
"Expected result (optional)": "Forventet resultat (valgfrit)",
"Explode": "Adskil",
"Explode Text": "Opløs tekst",
"Export Animation…": "Eksportér animation…",
"Export Bend Table…": "Eksportér bøjningstabel…",
"Export CSV…": "Eksportér CSV…",
"Export Current View as DXF": "Eksportér aktuel visning som DXF",
"Export Current View as DXF…": "Eksportér aktuel visning som DXF…",
"Export DXF": "Eksportér DXF",
"Export DXF…": "Eksportér DXF…",
"Export JSON…": "Eksportér JSON…",
"Export design library...": "Eksportér designbibliotek...",
"Export hidden edges as dashed lines": "Eksportér skjulte kanter som stiplede linjer",
"Exported by SolidWorks, which usually saves Y up": "Eksporteret af SolidWorks, som normalt gemmer med Y opad",
"Expression": "Udtryk",
"Extend to meet": "Forlæng, til de mødes",
"Extents": "Udstrækning",
"FILES": "FILER",
"FLAT": "FLAD",
"Face": "Flade",
"Face Appearance": "Fladens udseende",
"Face attachment lost": "Forbindelsen til fladen er mistet",
"Face center": "Fladens midtpunkt",
"Faces": "Flader",
"Faces / edges / vertices": "Flader / kanter / hjørnepunkter",
"Faces to split": "Flader, der skal opdeles",
"Fastener": "Befæstigelse",
"Favorites": "Favoritter",
"Favourite": "Favorit",
"Feature": "Feature",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Filer fra mange CAD-programmer har Y opad og åbnes derfor liggende på ryggen her (Z opad).",
"Filter": "Filter",
"Filter…": "Filtrér…",
"Find in Browser": "Find i modeltræ",
"Finish": "Afslut",
"First plane": "Første plan",
"Fit and G2 corners require Constant radius.": "Tilpasning og G2-hjørner kræver konstant radius.",
"Fixed Face": "Fast flade",
"Flange thickness": "Flangetykkelse",
"Flat Pattern": "Flad afvikling",
"Flat size": "Bredde mellem flader",
"Follow camera (Look At sketches)": "Følg kamera (Se på skitser)",
"Follow the camera (Look At sketches)": "Følg kameraet (Se på skitser)",
"Font": "Skrifttype",
"From": "Fra",
"From Bodies": "Fra emner",
"From edges": "Fra kanter",
"Full Length": "Fuld længde",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-hjørner kræver konstant radius og præcis tilpasning. Konstruktion af G2-hjørner understøttes endnu ikke.",
"Gap": "Spalte",
"Grid": "Gitter",
"HISTORY": "HISTORIK",
"Height": "Højde",
"Hidden edges": "Skjulte kanter",
"History layout": "Historiklayout",
"History names": "Navne i historikken",
"Hole (optional)": "Hul (valgfrit)",
"Holes": "Huller",
"Holes (edges on them)": "Huller (kanterne omkring dem)",
"Home view": "Hjemmevisning",
"Horiz./Vert.": "Vandr./Lodr.",
"I-Beam": "I-profil",
"IMAGES": "BILLEDER",
"Import…": "Importér…",
"In This Design": "I dette design",
"In place (surfaces)": "På stedet (overflader)",
"In plane": "I planet",
"Inside": "Indvendigt",
"Instant": "Øjeblikkelig",
"Interface font": "Skrifttype i brugergrænsefladen",
"Interferes": "Overlapper",
"Invert": "Invertér",
"Isocurve": "Isokurve",
"Issue title": "Titel på issue",
"Items": "Elementer",
"JOINTS": "LED",
"Joint Origin1": "Lednulpunkt1",
"Keep (points on it)": "Behold (punkter på fladen)",
"Keep Tools": "Behold værktøjsemner",
"Keep of the other surface": "Behold fra den anden overflade",
"Key a joint's value at this step": "Indsæt en nøgle for leddets værdi på dette trin",
"Keys": "Nøgler",
"L-Angle": "L-profil",
"LIBRARY": "BIBLIOTEK",
"LINKS": "LINKS",
"Later": "Senere",
"Leave it": "Lad det være",
"Left Middle": "Til venstre i midten",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Ældre fladetilknytning. Ny tilknytning til en certificeret flade giver en præcis tilknytning.",
"Length": "Længde",
"Length units": "Længdeenheder",
"Letter Spacing": "Tegnafstand",
"Library": "Bibliotek",
"Library file": "Biblioteksfil",
"Limits": "Grænser",
"Line Spacing": "Linjeafstand",
"Line Type": "Linjetype",
"Lines": "Linjer",
"Listing the installed fonts…": "Henter listen over installerede skrifttyper…",
"Load library": "Indlæs bibliotek",
"Local library": "Lokalt bibliotek",
"Loop": "Løkke",
"M5 or M5x0.5": "M5 eller M5x0.5",
"MANAGE": "ADMINISTRÉR",
"MATERIALS": "MATERIALER",
"MODEL": "MODEL",
@@ -210,18 +291,25 @@
"Matched restrictions check the final values.": "Matchede begrænsninger kontrollerer de endelige værdier.",
"Material": "Materiale",
"Max": "Maks.",
"Maximum": "Maksimum",
"Mesh body": "Meshlegeme",
"Mid Radius": "Mellemradius",
"Middle double-click": "Dobbeltklik med midterste museknap",
"Min": "Min.",
"Minimum": "Minimum",
"Mirror Plane": "Spejlingsplan",
"Mitre": "Gering",
"Model Parameters": "Modelparametre",
"Modeled": "Modelleret",
"More toolbar panels": "Flere værktøjspaneler",
"Motion": "Bevægelse",
"Mouse gestures": "Musebevægelser",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Musebevægelser virker kun med SolidWorks-musetilstanden (musehjulet roterer visningen, Ctrl + musehjul panorerer).",
"Move / Copy": "Flyt / Kopiér",
"Move Type": "Flytningstype",
"Move down": "Flyt ned",
"Move earlier": "Flyt tidligere",
"Move holes by clicking or dragging": "Flyt huller ved at klikke eller trække",
"Move later": "Flyt senere",
"Move up": "Flyt op",
"Name": "Navn",
@@ -235,17 +323,25 @@
"No matching appearance presets.": "Ingen matchende udseendeforudindstillinger.",
"No matching appearances": "Ingen matchende udseender",
"No visible bodies or sketches to export.": "Der er ingen synlige emner eller skitser at eksportere.",
"Nominal diameter": "Nominel diameter",
"None": "Ingen",
"Not available yet": "Endnu ikke tilgængelig",
"Notch": "Udskæring",
"OUTPUT": "OUTPUT",
"Of": "Af",
"On your computer": "På din computer",
"Opacity": "Opacitet",
"Open GitHub draft": "Åbn GitHub-kladde",
"Open library...": "Åbn bibliotek...",
"Open local libraries in the desktop app. You can export this design's library here.": "Åbn lokale biblioteker i desktopappen. Du kan eksportere dette designs bibliotek her.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Parametrisk 3D-CAD i Rust med åben kildekode. Licenseret under MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Åbner din browser og kopierer rapportteksten, så du kan indsætte den på GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Åbner din browser. Gennemgå og indsend rapporten på GitHub.",
"Operator": "Operator",
"Orbit (right drag)": "Rotér visningen (træk med højre museknap)",
"Other side": "Anden side",
"Other surface (mutual trim)": "Anden overflade (trim begge)",
"Outer radius": "Udvendig radius",
"Outside": "Udvendigt",
"Overall length": "Samlet længde",
"Param. Graph": "Param.graf",
@@ -258,8 +354,11 @@
"Pick the tool bodies": "Vælg værktøjsemnerne",
"Picked": "Valgt",
"Picked point": "Valgt punkt",
"Pitch": "Stigning",
"Pivot": "Drejepunkt",
"Place at the origin instead": "Placér ved nulpunktet i stedet",
"Plastic Rules": "Plastregler",
"Play the joint's motion in the view": "Afspil leddets bevægelse i visningen",
"Playback": "Afspilning",
"Plot": "Diagram",
"Position": "Position",
@@ -267,22 +366,29 @@
"Position (G0)": "Positionskontinuitet (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Position langs kanten: 0 ved den ene ende, 1 ved den anden",
"Preferences › Design turns the offer back on": "Indstillinger › Design slår tilbuddet til igen",
"Preview": "Forhåndsvisning",
"Preview Row": "Forhåndsvis række",
"Preview leaves the current design unchanged.": "Forhåndsvisning ændrer ikke det aktuelle design.",
"Profile / Edges": "Profil / kanter",
"Profiles or curves": "Profiler eller kurver",
"Profiles/Curves": "Profiler/kurver",
"Quantity": "Antal",
"RECENT": "SENESTE",
"REFERENCE": "REFERENCE",
"RULED SURFACE": "REGELFLADE",
"RULES": "REGLER",
"Radius": "Radius",
"Radius Fit": "Tilpasning af radius",
"Ratio": "Forhold",
"Reattach selected face": "Tilknyt den valgte flade igen",
"Recover": "Gendan",
"Recover unsaved design?": "Gendan design, der ikke er gemt?",
"Rectangular": "Rektangulær",
"Rectangular Tube": "Rektangulært hulprofil",
"Reference face": "Referenceflade",
"Remove Tools": "Fjern værktøjsemner",
"Remove condition": "Fjern betingelse",
"Remove from Folder": "Fjern fra mappe",
"Remove from list": "Fjern fra liste",
"Remove override": "Fjern tilsidesættelse",
"Remove private design details or paths before sharing.": "Fjern private designoplysninger eller filstier, før du deler.",
@@ -290,12 +396,18 @@
"Remove the column": "Fjern kolonnen",
"Remove the track": "Fjern sporet",
"Remove this point": "Fjern dette punkt",
"Rename Feature": "Omdøb feature",
"Rename Folder": "Omdøb mappe",
"Replace existing library file": "Erstat eksisterende biblioteksfil",
"Report a bug — review a screenshot and open a GitHub draft": "Rapportér en fejl — gennemgå et skærmbillede, og åbn en GitHub-kladde",
"Report text copied. Paste it into GitHub and submit your report there.": "Rapportteksten er kopieret. Indsæt den på GitHub, og indsend rapporten der.",
"Reports are public. A GitHub account is required.": "Rapporter er offentlige. Du skal have en GitHub-konto.",
"Reset Toolbar": "Nulstil værktøjslinje",
"Resize Part": "Ændr størrelse på del",
"Restore View": "Gendan visning",
"Restore outer boundary": "Gendan ydre afgrænsning",
"Restore the inherited component or physical material appearance": "Gendan udseendet fra komponenten eller det fysiske materiale",
"Restore this studio's tools": "Gendan værktøjerne i dette arbejdsområde",
"Result": "Resultat",
"Retake screenshot": "Tag skærmbillede igen",
"Reverse": "Vend om",
@@ -305,9 +417,20 @@
"Revert Pos.": "Fortryd pos.",
"Revert Position": "Tilbagefør position",
"Review the report and screenshot before opening GitHub.": "Gennemgå rapport og skærmbillede, før du åbner GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Gennemgå rapporten på GitHub, og klik på Submit new issue for at offentliggøre den.",
"Rib Length": "Ribbelængde",
"Rib Offset": "Ribbeforskydning",
"Rib Thickness": "Ribbetykkelse",
"Ribs": "Ribber",
"Right Middle": "Til højre i midten",
"Rim Edge": "Randkant",
"Ring of tools on the right button": "Værktøjsring på højre museknap",
"Roll Back Here": "Rul tilbage hertil",
"Roll History Marker Here": "Flyt tilbagerulningsmarkør hertil",
"Root radius": "Rodradius",
"Round": "Rund",
"Round Bar": "Rundstang",
"Round Tube": "Rundt hulprofil",
"Rule": "Regel",
"Rule name": "Regelnavn",
"Rules run from top to bottom. Matched drives change effective values.": "Regler køres oppefra og ned. Matchede drivere ændrer de effektive værdier.",
@@ -315,21 +438,29 @@
"SURFACE QUALITY": "OVERFLADEKVALITET",
"Save Rule": "Gem regel",
"Save View": "Gem visning",
"Save screenshot…": "Gem skærmbillede…",
"Save the study as an animated GIF, seen as the viewport shows it": "Gem undersøgelsen som en animeret GIF med samme visning som i modelvinduet",
"Saved preview. Geometry is being checked before editing.": "Gemt forhåndsvisning. Geometrien kontrolleres før redigering.",
"Scale factor": "Skaleringsfaktor",
"Scene": "Scene",
"Screenshot (optional)": "Skærmbillede (valgfrit)",
"Search": "Søg",
"Search commands": "Søg efter kommandoer",
"Search this library": "Søg i dette bibliotek",
"Second plane": "Andet plan",
"Section": "Snit",
"Section (optional)": "Tværsnit (valgfrit)",
"Section position": "Profilets placering",
"Sections (one sketch each)": "Tværsnit (én skitse til hvert)",
"Select Other": "Vælg andet",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vælg andet: Hold venstre museknap nede uden at flytte markøren på modellen (eller højreklik › Vælg andet…) for at vise alt under markøren. Klik igen samme sted for at vælge næste objekt. Dobbeltklik på den midterste museknap for at gå til Hjem (Indstillinger › Navigation).",
"Select bodies (a face picks its body)": "Vælg emner (en flade vælger sit emne)",
"Select bodies or faces instead": "Vælg emner eller flader i stedet",
"Select edges": "Vælg kanter",
"Selection filter": "Markeringsfilter",
"Semantic faces share their appearance across instances.": "Semantiske flader deler udseende på tværs af instanser.",
"Send your report on GitHub": "Send din rapport på GitHub",
"Shaded": "Skyggelagt",
"Shape": "Form",
"Sheet Rules": "Pladeregler",
"Show Agent Cursor": "Vis agentmarkør",
@@ -345,9 +476,11 @@
"Sketch plane has no body-face attachment.": "Skitseplanet er ikke tilknyttet en flade på et emne.",
"Sketch points": "Skitsepunkter",
"Slot width": "Slidsbredde",
"Snap to face and edge centres": "Snap til flade- og kantcentre",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft lukkede, før disse designs blev gemt. Den seneste automatiske lagring kan åbnes.",
"Specialise": "Specialisér",
"Speed": "Hastighed",
"Splitting tool": "Opdelingsværktøj",
"Square": "Kvadrat",
"Stand it up": "Stil op",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stil op: En kvart omdrejning om X som én flyttefeature, der kan fortrydes.",
@@ -357,17 +490,27 @@
"Start continuity": "Overgang ved starten",
"Step": "Trin",
"Steps": "Trin",
"Steps to reproduce (optional)": "Trin til at genskabe problemet (valgfrit)",
"Stitch the kept pieces": "Sammenføj de bevarede dele",
"Stitch to both bodies": "Sammenføj med begge emner",
"Studio": "Arbejdsområde",
"Study": "Undersøgelse",
"Style": "Stil",
"Summary": "Resumé",
"Suppress": "Undertryk",
"Suppressed": "Undertrykt",
"Surface": "Overflade",
"Surface, or faces to turn over": "Overflade eller flader, der skal vendes",
"Swap Ends": "Ombyt ender",
"TRIM": "TRIM",
"Tangent (G1)": "Tangentkontinuitet (G1)",
"Tangent Chain": "Tangentkæde",
"Tangent chain": "Tangentkæde",
"Tap drill diameter": "Diameter på gevindbor",
"Target": "Mål",
"Target Body": "Mållegeme",
"Tear": "Rivning",
"Technical details": "Tekniske detaljer",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Den stiplede skitse er en gemt planreference. Vælg en ny flade for at reparere tilknytningen.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Den gemte oprindelige komponentforekomst mangler. Vælg en gyldig oprindelig placering før ny tilknytning.",
"Then": "Så",
@@ -379,24 +522,36 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Dette underobjekt er delt. Sletning fjerner det fra alle kopier af dets overordnede objekt.",
"This child is shared. Position changes apply to every copy of its parent.": "Dette underobjekt er delt. Positionsændringer gælder for alle kopier af dets overordnede objekt.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Denne flade kan ikke give en varig tilknytning. Vælg en anden flade, eller vælg udtrykkeligt et fast plan.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Dette billede er endnu ikke vedhæftet på GitHub. Kopiér eller gem det, og vedhæft det derefter på GitHub.",
"To": "Til",
"To Offset": "Til forskydning",
"To edges": "Til kanter",
"Toe radius": "Tåradius",
"Tolerance": "Tolerance",
"Too close": "For tæt på",
"Tool Bodies": "Værktøjslegemer",
"Tools": "Værktøjer",
"Tools, clockwise from the top": "Værktøjer, med uret fra toppen",
"Top Left": "Øverst til venstre",
"Top Middle": "Øverst i midten",
"Top Right": "Øverst til højre",
"Trimming tool (plane, face or curves)": "Trim med plan, flade eller kurver",
"Turn off section analysis before exporting the view.": "Slå snitanalyse fra, før visningen eksporteres.",
"Type": "Type",
"UNTRIM": "FJERN BESKÆRING",
"Unit": "Enhed",
"Unsuppress": "Ophæv undertrykkelse",
"Updating the preview": "Opdaterer forhåndsvisningen",
"Use active author placement": "Brug den aktive komponents placering",
"Use fixed plane": "Brug fast plan",
"Use saved fixed plane": "Brug gemt fast plan",
"Use selected face as fixed plane": "Brug valgt flade som fast plan",
"Use the SolidWorks mouse mode": "Brug SolidWorks-musetilstanden",
"User Parameters": "Brugerparametre",
"VIEW": "VISNING",
"VIEWS": "VISNINGER",
"Value": "Værdi",
"Variable Radius": "Variabel radius",
"Velocity": "Hastighed",
"Vertices": "Hjørnepunkter",
"View navigator": "Visningsnavigator",
@@ -404,16 +559,53 @@
"Wall": "Væg",
"Wall Location": "Vægplacering",
"Wall Thickness": "Vægtykkelse",
"Web thickness": "Kroptykkelse",
"What did you do just before the problem? A short list is enough": "Hvad gjorde du lige før problemet opstod? En kort liste er nok",
"What did you expect to see?": "Hvad forventede du at se?",
"What happened?": "Hvad skete der?",
"When": "Når",
"Width": "Bredde",
"Will fail": "Vil fejle",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Når musebevægelser er slået til, roterer højre museknap ikke længere visningen: Hold den nede, og bevæg musen for at åbne ringen. Før derefter markøren ind i et værktøjs område. Et almindeligt højreklik åbner stadig menuen.",
"X": "X",
"X Angle": "X-vinkel",
"X angle": "X-vinkel",
"X distance": "X-afstand",
"Y": "Y",
"Y Angle": "Y-vinkel",
"Y angle": "Y-vinkel",
"Y distance": "Y-afstand",
"Z": "Z",
"Z Angle": "Z-vinkel",
"Z Distance": "Z-afstand",
"Z angle": "Z-vinkel",
"Z distance": "Z-afstand",
"Zebra": "Zebra",
"ai.prefs.allow_control": "Tillad AI-agenter at oprette forbindelse (kontrolkanal / MCP)",
"ai.prefs.allow_control_note": "Åbner kun en kontrolkanal på denne computer (127.0.0.1, port 27878 eller den næste ledige). Et program, der opretter forbindelse, skal også fremvise et hemmeligt token, som kun din brugerkonto kan læse, og som oprettes på ny ved hver start. Træder i kraft, næste gang SolidCraft starter. Fra: Ingen port lytter, medmindre du starter SolidCraft med --control.",
"ai.prefs.availability": "Assistentstyring",
"ai.prefs.background_commands": "Kør agentkommandoer i baggrunden",
"ai.prefs.follow_camera": "Vend kameraet mod skitser",
"ai.prefs.follow_camera_off": "Automatisk kamerafølgning er slået fra.",
"ai.prefs.follow_camera_on": "Kameraet vender sig mod skitser, som agenten opretter.",
"ai.prefs.intro": "Agenter, tilladelser og visning af deres arbejde.",
"ai.prefs.make_default": "Brug som standard",
"ai.prefs.no_agents": "Ingen agenter endnu. Tilføj en agent, eller opdatér for at finde installerede agenter.",
"ai.prefs.no_saved_approvals": "Ingen gemte godkendelser.",
"ai.prefs.override_all_warning": "Denne agent kan gemme, overskrive, eksportere eller erstatte designet uden at spørge.",
"ai.prefs.permission_all": "Tillad alt",
"ai.prefs.permission_all_summary": "Tilgængelige SolidCraft-værktøjer kører uden at spørge.",
"ai.prefs.permission_ask": "Spørg før ændringer",
"ai.prefs.permission_ask_summary": "Godkend hver modelændring og filhandling.",
"ai.prefs.permission_edits": "Tillad modelændringer",
"ai.prefs.permission_edits_summary": "Modelændringer udføres; filhandlinger og destruktive handlinger kræver stadig godkendelse.",
"ai.prefs.permission_scope": "Gælder kun SolidCraft-værktøjer. Agentens shell og filændringer bruger dens egne tilladelser.",
"ai.prefs.pointer_preview_hidden": "Markør skjult",
"ai.prefs.pointer_preview_speed": "Agentmarkør · {speed}",
"ai.prefs.pointer_speed": "Bevægelseshastighed",
"ai.prefs.privacy_note": "Kun din valgte agent eller dit valgte slutpunkt modtager samtalen. Slutpunktsnøgler gemmes i systemets nøglering.",
"ai.prefs.show_pointer": "Vis agentmarkør",
"ai.prefs.turn_limit_hint": "Det maksimale antal værktøjskald, der ændrer designet, som assistenten må foretage for én besked.",
"angle, height": "vinkel, højde",
"appearance.dielectric_ior": "Dielektrisk brydningsindeks",
"appearance.metallic": "Metallisk",
@@ -460,8 +652,10 @@
"cmd.component_activate": "Aktivér komponent",
"cmd.component_break_link": "Bryd forbindelse",
"cmd.component_capture_position": "Registrér position",
"cmd.component_copies": "Find identiske kopier",
"cmd.component_create": "Ny komponent",
"cmd.component_delete": "Slet komponent",
"cmd.component_extract": "Udtræk til fil",
"cmd.component_from_bodies": "Opret komponenter fra emner",
"cmd.component_insert_design": "Indsæt design",
"cmd.component_links": "Vis forbindelser",
@@ -474,6 +668,7 @@
"cmd.component_properties": "Komponentens fysiske egenskaber",
"cmd.component_relink": "Skift kilde",
"cmd.component_rename": "Omdøb komponent",
"cmd.component_replace_with_link": "Erstat med link",
"cmd.component_revert_position": "Tilbagefør position",
"cmd.component_unlock": "Lås indhold op",
"cmd.component_update_links": "Opdatér forbindelser",
@@ -596,8 +791,10 @@
"cmd.motion_plot": "Vis diagram for bevægelsesanalyse",
"cmd.motion_study": "Bevægelsesanalyse",
"cmd.motion_trace": "Bevægelsesspor",
"cmd.motion_trace_edit": "Redigér bevægelsesspor",
"cmd.motion_trace_keep": "Behold bevægelsesspor",
"cmd.occurrence_copy": "Indsæt (komponentforekomst)",
"cmd.occurrence_drag": "Træk forekomst",
"cmd.occurrence_ground": "Fastgør",
"cmd.occurrence_move": "Flyt komponentforekomst",
"cmd.occurrence_properties": "Komponentforekomstens fysiske egenskaber",
@@ -655,6 +852,7 @@
"cmd.sketch_auto_constrain_from_datum": "Automatisk binding ud fra reference",
"cmd.sketch_auto_project": "Projektér referencekanter automatisk",
"cmd.sketch_blend_curve": "Overgangskurve",
"cmd.sketch_cancel": "Annullér skitse",
"cmd.sketch_chain": "Kæde",
"cmd.sketch_chamfer_distance_angle": "Affasning med afstand og vinkel",
"cmd.sketch_chamfer_equal_distance": "Affasning med ens afstande",
@@ -770,6 +968,7 @@
"cmd.view_graphics_preset": "Grafikprofil",
"cmd.view_list": "Vis navngivne visninger",
"cmd.view_navigate": "Navigér i visning",
"cmd.view_normal_to": "Vinkelret på markering",
"cmd.view_rename": "Omdøb navngiven visning",
"cmd.view_save": "Ny navngiven visning",
"color.copy": "Klik for at kopiere farveværdier",
@@ -858,6 +1057,7 @@
"file_load.stopping": "Venter på, at den forrige handling stopper...",
"file_load.waiting": "Venter på filhandling",
"fits the face, e.g. M8": "passer til fladen, f.eks. M8",
"from the profile": "fra profilen",
"from the rule": "fra reglen",
"help.apache": "Apache-licens 2.0",
"help.attributions": "Tilskrivninger (ATTRIBUTION.md)",
@@ -867,6 +1067,12 @@
"help.licences": "Licenser",
"help.mit": "MIT-licens",
"help.notice": "NOTICE",
"help.solid.chamfer": "Affasning skærer de valgte kanter skråt med den angivne afstand. Brug den til at fjerne skarpe kanter eller lette indføringen af en tap eller skrue.",
"help.solid.extrude": "Ekstrudering giver en skitseprofil dybde ved at tilføje eller fjerne materiale langs en retning. Angiv afstanden for at danne en klods, en forhøjning eller en lomme. Brug Udhulning, hvis en massiv model i stedet skal gøres hul med en bestemt vægtykkelse.",
"help.solid.fillet": "Afrunding erstatter de valgte kanter med en glat, cirkulær overgang med den angivne radius. Den kan blødgøre skarpe kanter af hensyn til styrke, komfort eller udseende.",
"help.solid.hole": "Hul opretter et rundt hul ved et valgt punkt på en flade. Angiv diameteren, og vælg eventuelt konisk forsænkning, cylindrisk forsænkning eller gevind. Ved gevind vælges en gevindbetegnelse, f.eks. M6.",
"help.solid.revolve": "Omdrejning drejer en skitseprofil omkring en valgt akse og danner et omdrejningslegeme. Brug den f.eks. til aksler, knopper og skåle, der er runde omkring en centerlinje.",
"help.solid.shell": "Udhulning gør et massivt legeme hult med den angivne vægtykkelse. Vælg de flader, der skal åbnes. Indvendig udhulning bevarer de udvendige mål, mens udvendig udhulning bevarer de indvendige mål. Brug den f.eks. til kasser, dæksler og andre tyndvæggede dele.",
"help.third_party": "Tredjepartslicenser",
"home.back": "Tilbage til {name}",
"home.crashes": "Nedbrudsrapporter",
@@ -971,11 +1177,21 @@
"motion.key_here": "Sæt punkt her",
"motion.key_here_hint": "Tilføj et animationspunkt i dette trin med ledets aktuelle værdi",
"motion.no_collisions": "Ingen kollisioner under analysen",
"motion.plot_quantity": "Antal",
"motion.step_back": "Trin tilbage",
"motion.step_forward": "Trin frem",
"motion.to_end": "Til slutningen",
"motion.to_start": "Til starten",
"motion.track_name": "Spor {number}",
"nav.alias.help": "Shift+Alt+træk med venstre museknap roterer visningen, Shift+Alt+midterste panorerer, Shift+Alt+højre zoomer; træk med midterste museknap panorerer også, og træk med højre roterer visningen",
"nav.blender.help": "Træk med midterste museknap roterer visningen, Shift+midterste panorerer, Ctrl+midterste zoomer; træk med højre museknap påvirker ikke kameraet",
"nav.free_orbit.help": "Musen og 3D-musen roterer gennem begge poler og bevarer rulning. Snap til akser med Shift er slået fra.",
"nav.free_orbit.label": "Fri rotation af visningen",
"nav.fusion.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, træk med højre roterer visningen",
"nav.inventor.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, Ctrl+midterste zoomer, træk med højre roterer visningen",
"nav.openscad.help": "Træk med venstre museknap i et tomt område roterer visningen, Shift+venstre markerer med en ramme, træk i skitsegeometri redigerer den; træk med højre eller midterste museknap panorerer, Shift+højre eller Shift+midterste zoomer",
"nav.solidworks.help": "Træk med midterste museknap roterer visningen, Ctrl+midterste panorerer, Shift+midterste zoomer, træk med højre roterer visningen",
"nav.tinkercad.help": "Træk med højre museknap roterer visningen, træk med midterste eller Shift+højre panorerer",
"nav.touchpad.help": "Hold Shift nede, og bevæg markøren for at panorere; hold Alt/Option nede, og bevæg den for at rotere visningen. Ingen museknap behøver at være trykket ned. Lodret rulning zoomer.",
"nav.touchpad.label": "Pegefelt",
"nav.touchpad.orbit": "Rotér visning (hold Alt/Option nede og bevæg markøren; ingen museknap)",
@@ -998,8 +1214,130 @@
"pattern.flip_direction": "Vend retning",
"pick a hole to seat it, or OK places it at the origin": "vælg et hul til placeringen, eller tryk OK for at placere ved nulpunktet",
"pick one or two vertices, edges or faces": "vælg et eller to hjørnepunkter, kanter eller flader",
"prefs.add_item": "Tilføj elementet",
"prefs.alt_frees_manipulators": "Alt / Option frigør også manipulatorpilene",
"prefs.anti_aliasing_note": "Udjævner takkede kanter.",
"prefs.applies_on_restart": "Træder i kraft efter genstart",
"prefs.assistant": "Assistent",
"prefs.axes": "Akser",
"prefs.axis": "Akse",
"prefs.bottom": "Bund",
"prefs.browser": "Modelbrowser",
"prefs.built_in": "Indbygget",
"prefs.button_left": "Træk med venstre museknap",
"prefs.button_middle": "Træk med midterste museknap",
"prefs.button_right": "Træk med højre museknap",
"prefs.choose_font": "Vælg skrifttypefil…",
"prefs.click": "Klik",
"prefs.contact_shading": "Blød kontaktskygning",
"prefs.contact_shading_note": "Blød skygning, hvor dele er tæt på hinanden.",
"prefs.context_menu": "Genvejsmenu",
"prefs.context_menu_ring": "Genvejsmenu · hold nede for at åbne den radiale menu",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Åbn nedbrudsrapporter…",
"prefs.crash_reports_note": "Rapporter gemmes på denne computer med designet og de kommandoer, der genskaber problemet.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Krumme overflader",
"prefs.curves": "Krumme overflader",
"prefs.dead_zone_note": "Det skraverede bånd er dødzonen: Bevægelser inden for det ignoreres.",
"prefs.default": "standard",
"prefs.design": "Standardindstillinger for design",
"prefs.design_note": "Udgangspunktet for nye design. Åbne design beholder deres egne indstillinger.",
"prefs.devices": "3D-mus",
"prefs.display": "Visning",
"prefs.edges": "Kanter og skygning",
"prefs.extend": "Udvid",
"prefs.extra_navigation": "Ctrl+træk med venstre museknap i et tomt område roterer visningen; hold mellemrumstasten nede for at panorere.",
"prefs.follows_os": "Følger operativsystemet",
"prefs.font": "Skrifttype",
"prefs.font_fallback": "Tegn, som den valgte skrifttype mangler, vises med den indbyggede skrifttype, så ingen tekst viser en tom firkant.",
"prefs.font_sample": "Høj bly gom vandt fræk sexquiz · Ø12,50 mm · 45,0° · 0123456789",
"prefs.font_unreadable": "Den valgte skrifttype kunne ikke læses.",
"prefs.general": "Generelt",
"prefs.general_note": "Sprog, lagring og opdateringer.",
"prefs.gesture_orbit": "Rotér visningen",
"prefs.gesture_pan": "Panorér",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Grafik",
"prefs.graphics_note": "Afvej hastighed mod udjævning. Hvert valg viser sin virkning på et eksempel.",
"prefs.graphics_presets_note": "Forudindstillinger angiver de nedenstående valg samlet.",
"prefs.history": "Historik",
"prefs.hold_stop_snapping": "Hold nede for at slå snapping fra",
"prefs.icons_only": "Kun ikoner",
"prefs.imported_file": "Importeret fil",
"prefs.input": "Input",
"prefs.interface": "Brugergrænseflade",
"prefs.interface_language": "Sprog i brugergrænsefladen",
"prefs.interface_note": "SolidCrafts udseende og placeringen af dets dele.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Etiketter",
"prefs.layout": "Layout",
"prefs.layout_note": "Kompakt fastgør panelerne til vinduets kanter. Gennemsigtighed i modelbrowseren er tilgængelig i Flydende. Skift assistentens side på siden Assistent.",
"prefs.left": "Venstre",
"prefs.left_click": "Venstreklik",
"prefs.live_input": "Løbende input",
"prefs.live_preview": "Løbende forhåndsvisning",
"prefs.minutes": "minutter",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mus og pegefelt",
"prefs.mouse_heading": "Mus",
"prefs.mouse_mapping": "Musetildeling",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "Hvordan musen, pegefeltet og 3D-musen styrer visningen.",
"prefs.not_on_gpu": "ikke på denne GPU",
"prefs.on": "Til",
"prefs.opaque": "Uigennemsigtig",
"prefs.orbit_snap": "Hold Shift nede under rotation af visningen for at snappe til en akse.",
"prefs.orientation": "Orientering",
"prefs.overall_speed": "Samlet hastighed",
"prefs.preview_choices": "Forhåndsvisning af dine valg",
"prefs.quality_default": "standard",
"prefs.quality_fastest": "hurtigst",
"prefs.quality_slowest": "langsomst",
"prefs.quality_smoother": "mere udjævnet",
"prefs.reports_saved": "{n} gemt",
"prefs.right_click": "Højreklik",
"prefs.saving": "Automatisk lagring og gendannelse",
"prefs.scale_note": "Også Ctrl + / Ctrl − i vinduet. Knapper er 28 px ved 100%.",
"prefs.scale_text": "Skalering og tekst",
"prefs.select": "Vælg",
"prefs.select_one_item": "Vælg ét element",
"prefs.selecting": "Markering",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift tilføjer kun; Ctrl / Cmd skifter markeringen",
"prefs.shift_add_note": "Til: Shift tilføjer, Ctrl / Cmd skifter markeringen. Fra (standard): Begge skifter markeringen.",
"prefs.shortcuts": "Genveje",
"prefs.sketch_selection": "Skitse og markering",
"prefs.sketch_selection_heading": "Skitse og markering",
"prefs.sketch_selection_note": "Hvordan skitser snapper, og hvordan klik markerer.",
"prefs.sketching": "Skitsering",
"prefs.snap_drawing_note": "Mens du tegner eller trækker et skitsepunkt.",
"prefs.source": "Kilde",
"prefs.speed": "Hastighed",
"prefs.stand_up_note": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X.",
"prefs.status_done": "Færdig",
"prefs.status_error": "Fejl",
"prefs.status_selected": "Valgt",
"prefs.status_warning": "Advarsel",
"prefs.supersampling_brief": "Tegner visningen i større størrelse og nedskalerer den med filtrering: meget skarpt og mere krævende.",
"prefs.tab_accent_note": "Accentfarven følger den aktive fane",
"prefs.theme_file": "temafil",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nu",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Tilføj elementet, eller fjern det igen",
"prefs.toolbar": "Værktøjslinje",
"prefs.translucent": "Halvgennemsigtig",
"prefs.under_browser": "Under modelbrowseren",
"prefs.under_cursor": "Under markøren",
"prefs.units_precision": "Enheder og præcision",
"prefs.updates": "Opdateringer",
"prefs.view": "Visningens adfærd",
"prefs.viewport": "Visningsområde",
"prefs.wheel": "Musehjul",
"prefs.wheel_trackpad": "Musehjul og pegefelt",
"prefs.workspace": "Arbejdsområde",
"prefs.zoom_toward_cursor": "Zoom mod markøren",
"properties.title": "Egenskaber: {name}",
"recovery.days_ago": "for {count} dage siden",
"recovery.detail": "{file} · {features} features · autogemt {when}",
@@ -1070,16 +1408,20 @@
"search.suggest": "Mente du {label}?",
"search.try": "Skriv et kommandonavn, en feature i dette design eller en parameter. Du kører en kommando med værdier ved at skrive dens id og et JSON-objekt: {example}",
"search.valid": "Gyldig",
"settings.display_quality_note": "Finere niveauer viser krumme flader og kanter mere jævnt og tager længere tid at opbygge mesh til. Kun modelvisningen ændres, aldrig designet eller eksporten.",
"section.off_hint": "Intet plan valgt: OK slår snittet fra.",
"settings.display_quality_note": "Gælder kun visningen. Design og eksport påvirkes ikke.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen.",
"settings.measure_axis_distances": "Vis X/Y/Z-afstande",
"settings.measure_axis_distances_help": "Vis afstanden langs X-, Y- og Z-aksen sammen med den samlede afstand i Mål.",
"settings.msaa_applies_on_restart": "Kantudjævning træder i kraft, næste gang SolidCraft starter",
"settings.msaa_reset": "{setting}: {requested}× understøttes ikke af denne grafikenhed; værdien er nu {reset}× (understøttet: {supported}).",
"settings.msaa_unsupported": "Understøttes ikke af denne grafikenhed",
"settings.reference_geometry_through_bodies": "Vis referencegeometri gennem legemer",
"settings.reference_geometry_through_bodies_help": "Tegn inaktive skitsekonturer og referenceplaner oven på skyggelagte legemer",
"settings.reset_intro": "Disse indstillinger virker ikke på denne computer og ville forhindre SolidCraft i at starte. De er derfor ændret:",
"settings.reset_title": "Indstillinger blev ændret",
"settings.supersampling_note": "Tegner visningen med højere opløsning og nedskalerer den: Jævnere kanter, linjer og gitter på skærme under 4K. Det kræver GPU-tid: ca. 2,25× så mange pixels ved 1,5× og 4× ved 2×.",
"sheet.no_body": "der er intet pladelegeme: Start et med Flange > Basis, eller omdan en plade til et pladelegeme med Konvertér til plade (vælg dens store plane flade)",
"shortcut.json_error": "parametre skal være JSON: {error}",
"shortcut.no_command": "ingen kommando `{id}`; teksten før JSON skal være et kommando-id",
"shortcut.pin": "Fastgør øverst i S-boksen",
@@ -1116,6 +1458,9 @@
"thread.designation": "Betegnelse",
"thread.iso_metric": "ISO-metrisk",
"thread.select_size": "Vælg størrelse",
"timeline.delete_after_marker_one": "Slet 1 feature efter historikmarkøren",
"timeline.delete_after_marker_other": "Slet {count} features efter historikmarkøren",
"timeline.delete_many": "Slet {count} valgte features",
"timeline.group": "Ny featuremappe",
"timeline.move_many": "Flyt elementer i tidslinjen",
"timeline.move_to_group": "Flyt features til mappe",
@@ -1361,11 +1706,15 @@
"ui.agent_chat_waiting": "Venter på dig",
"ui.agent_cursor": "Agentmarkør",
"ui.agent_cursor_speed": "Agentmarkørens hastighed",
"ui.agent_off_thread": "Hold programvinduet responsivt, mens agenter udfører kommandoer",
"ui.agent_off_thread_note": "Langsomme kommandoer fra en agent (via AI-panelet eller en MCP-klient) køres i en separat tråd, så du stadig kan rotere og undersøge modellen. Du kan stoppe en kommando via statusfeltet i visningen. Når funktionen er slået fra, køres kommandoerne på programvinduets tråd, og vinduet venter, indtil de er færdige.",
"ui.ai_add": "Tilføj",
"ui.ai_add_agent": "Tilføj agent…",
"ui.ai_add_agent_title": "Tilføj agent",
"ui.ai_add_anthropic": "Anthropic-kompatibel API",
"ui.ai_add_api": "OpenAI-kompatibel API",
"ui.ai_add_custom": "Brugerdefineret kommando",
"ui.ai_add_endpoint": "API-slutpunkt",
"ui.ai_add_installed": "Installeret agent",
"ui.ai_add_installed_note": "Din egen opsætning af en installeret agent, f.eks. med andre argumenter. Installerede agenter vises automatisk.",
"ui.ai_added": "tilføjet",
@@ -1389,11 +1738,13 @@
"ui.ai_http_busy": "endepunktet er optaget eller har nået sin grænse; prøv igen senere",
"ui.ai_http_key": "endepunktet afviste nøglen: Kontrollér den i Indstillinger > AI",
"ui.ai_http_not_found": "endepunktet eller modellen findes ikke: Kontrollér basis-URL og modelnavn",
"ui.ai_http_too_large": "samtalen er for stor til dette endpoint; start en ny chat",
"ui.ai_key": "API-nøgle",
"ui.ai_key_hint": "valgfri; gemmes i systemets nøglering",
"ui.ai_key_needs_https": "En nøgle sendes kun via HTTPS eller til denne computer.",
"ui.ai_key_note": "Gemmes i systemets nøglering, aldrig i SolidCrafts indstillinger. Lad feltet være tomt ved en lokal server.",
"ui.ai_keychain_error": "Systemets nøglering afviste anmodningen: {error}",
"ui.ai_kind": "Type",
"ui.ai_mode_all": "Tillad alt",
"ui.ai_mode_all_note": "Intet kræver godkendelse først. Nedbrudsrapporter er stadig utilgængelige, og mappebegrænsningerne gælder fortsat.",
"ui.ai_mode_all_warning": "Agenten kan gemme, overskrive, eksportere og erstatte dit design uden at spørge. Brug kun dette med en agent, du har tillid til, og på designs, du har gemt.",
@@ -1415,6 +1766,7 @@
"ui.ai_panel": "Panel",
"ui.ai_permissions": "Tilladelser",
"ui.ai_preset": "Forudindstilling",
"ui.ai_protocol": "Protokol",
"ui.ai_remove": "Fjern",
"ui.ai_reset": "Nulstil",
"ui.ai_settings": "AI-indstillinger…",
@@ -1424,6 +1776,8 @@
"ui.ai_test": "Test",
"ui.ai_test_acp_ok": "Svarer: {agent}",
"ui.ai_test_acp_tip": "Starter agenten kortvarigt og anmoder om en session; afslutter den derefter igen",
"ui.ai_test_answered": "Forbindelse oprettet: {model} svarede",
"ui.ai_test_anthropic_tip": "Sender en kort besked til slutpunktet",
"ui.ai_test_api_tip": "Henter endepunktets modeller",
"ui.ai_test_no_model": "Forbindelse oprettet: {count} modeller, men ikke {model}",
"ui.ai_test_ok": "Forbindelse oprettet: {count} modeller",
@@ -1433,8 +1787,8 @@
"ui.ai_tool_running": "kører",
"ui.ai_tool_unfold": "Klik for at vise svaret",
"ui.ai_tool_waiting": "venter",
"ui.ai_toolbar_button": "Vis knap på værktøjslinjen",
"ui.ai_toolbar_button_note": "AI-knappen i højre ende af værktøjslinjen. Ctrl+Shift+A og menuen Vis åbner stadig panelet.",
"ui.ai_toolbar_button": "Vis Assistent-knappen på programlinjen",
"ui.ai_toolbar_button_note": "Assistent-knappen på programlinjen. Du kan stadig åbne panelet med Ctrl+Shift+A eller via menuen Vis.",
"ui.ai_undo_tip": "Fortryd alt, agenten ændrede for denne besked, som ét trin",
"ui.ai_url_hint": "https://example.com/v1",
"ui.all": "Alle",
@@ -1582,7 +1936,12 @@
"ui.export_keymap_tip": "Gem tastaturgenveje, favoritter i S-menuen og værktøjslinjen i en fil",
"ui.extend": "Forlæng",
"ui.extent_type": "Udstrækningstype",
"ui.extract_component": "Udtræk til fil…",
"ui.extract_design_changed": "Designet blev ændret, mens du valgte en fil. Start Udtræk til fil igen.",
"ui.extract_done": "Komponenten er nu et selvstændigt design, der er linket hertil.",
"ui.extrude": "Ekstrudering",
"ui.extrude_auto_to_object": "Ekstrudér til et valgt mål",
"ui.extrude_auto_to_object_tip": "Når en profil er valgt i Ekstrudering, skifter udstrækningen til Til objekt, hvis du vælger en flade, et hjørnepunkt eller et parallelt plan uden for profilens plan",
"ui.factor": "Faktor",
"ui.feature_history": "Featuretidslinje",
"ui.feature_options": "Featureindstillinger",
@@ -1592,6 +1951,8 @@
"ui.filter_commands_or_keys": "Filtrér kommandoer eller taster",
"ui.findInTimeline": "Find i tidslinje",
"ui.findInWindow": "Find i vindue",
"ui.fine": "Fin",
"ui.finest": "Fineste",
"ui.finish_sketch": "Afslut skitse",
"ui.fit": "Tilpas",
"ui.fix_unfix": "Lås fast/frigør",
@@ -1680,6 +2041,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "VÆRKTØJER",
"ui.heading_utility": "VÆRKTØJ",
"ui.heading_weldments": "SVEJSNING",
"ui.help": "Hjælp",
"ui.hide": "Skjul",
"ui.hideAxes": "Skjul akser",
@@ -1689,10 +2051,17 @@
"ui.hideProfile": "Skjul profil",
"ui.history_back": "Gå ét trin tilbage",
"ui.history_below_browser": "Under browseren",
"ui.history_edge_hint": "dobbeltklik på kanten",
"ui.history_first": "Gå til begyndelsen",
"ui.history_forward": "Gå ét trin frem",
"ui.history_hide_names": "Skjul navne",
"ui.history_last": "Gå til slutningen",
"ui.history_marker_tip": "Historikmarkør: Træk for at rulle modellen tilbage; nye features indsættes her",
"ui.history_names": "Navne",
"ui.history_play": "Afspil til slutningen",
"ui.history_show_names": "Vis navne",
"ui.history_title": "Historik",
"ui.history_toggle_names": "Vis eller skjul featurenavne",
"ui.hole": "Hul",
"ui.hole_tap_type": "Hullets gevindtype",
"ui.hole_type": "Hultype",
@@ -1714,6 +2083,10 @@
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
"ui.link_copies": "Link identiske kopier",
"ui.link_copies_keep": "Behold",
"ui.link_copies_link": "Link",
"ui.link_copies_text": "Disse komponenter danner de samme legemer som {name}. Skal de gøres til placeringer af dens link?",
"ui.lock": "Lås",
"ui.loft": "Loft",
"ui.look_at": "Se på",
@@ -1816,6 +2189,7 @@
"ui.project_face_edges_sketch": "Projektér fladekanter til nye skitser",
"ui.properties": "Egenskaber",
"ui.pull_direction": "Udtræksretning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Afslut",
@@ -1827,6 +2201,9 @@
"ui.rectangle": "Rektangel",
"ui.rectangular_pattern": "Rektangulært mønster",
"ui.redo": "Gentag",
"ui.reference_1": "Reference 1",
"ui.reference_2": "Reference 2",
"ui.reference_distance": "Referenceafstand",
"ui.relink_design_changed": "Samlingen blev ændret, mens du valgte en kilde. Start Skift kilde igen.",
"ui.relink_done": "Kilden er ændret for alle forekomster.",
"ui.relink_not_linked": "Denne komponent er ikke knyttet til et kildedesign.",
@@ -1837,6 +2214,7 @@
"ui.render": "Rendering",
"ui.renderer": "Renderingsmotor",
"ui.repeat": "Gentag",
"ui.replace_with_link_done": "Kopierne er nu placeringer af det linkede design.",
"ui.report_a_bug": "Rapportér en fejl",
"ui.reset": "Nulstil",
"ui.reset_all": "Nulstil alle",
@@ -1875,6 +2253,12 @@
"ui.sent_to_slicer": "Sendt til {slicer}",
"ui.sheet_gap": "Mellemrum",
"ui.shell": "Udhulning",
"ui.shortcut_capture_add_key": "Tilføj én almindelig tast. Escape annullerer.",
"ui.shortcut_capture_hint": "Klik på en tastetildeling, og tryk derefter på modifikationstaster og én tast. Tilføj modifikationstaster inden for ét sekund; slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_press": "Tryk på modifikationstaster og én tast. Escape annullerer.",
"ui.shortcut_capture_release": "Slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_settling": "Bekræfter genvej… Escape annullerer.",
"ui.shortcut_multiple_keys": "Brug modifikationstaster og kun én anden tast. Klik på tastetildelingen for at prøve igen.",
"ui.show": "Vis",
"ui.showAll": "Vis alle",
"ui.showAllBodies": "Vis alle emner",
@@ -1910,6 +2294,7 @@
"ui.spline": "Spline",
"ui.split_body": "Opdel emne",
"ui.stand_up_tip": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Start forfra",
"ui.start_page": "Startside",
@@ -1937,7 +2322,7 @@
"ui.tie_ends": "Sammenbind",
"ui.tie_ends_tip": "Bind alle ubundne ender",
"ui.timeline_bottom": "Tidslinje nederst",
"ui.timeline_left_note": "Layoutet til venstre viser tilbagerulningslinjen over modeltræet med emner og scene.",
"ui.timeline_left_note": "Den lodrette historik vises til venstre for modelbrowseren. Featurenavne kan vises eller skjules på strimlen.",
"ui.timeline_left_sidebar": "Venstre sidepanel",
"ui.tip_angle": "Spidsvinkel",
"ui.to_object": "Til objekt",
@@ -1945,7 +2330,7 @@
"ui.top": "Ovenfra",
"ui.torus": "Torus",
"ui.total_angle": "Samlet vinkel",
"ui.trackpad_pan_tip": "Bevægelser på pegefeltet panorerer (slået fra: zoomer som musehjulet); musehjulet zoomer, tommelfingerhjulet panorerer sidelæns, og knibebevægelse zoomer altid",
"ui.trackpad_pan_tip": "Rulning med to fingre panorerer; i Fusion roterer Shift + to fingre visningen. Fra: Rulning zoomer. Musehjul og knibebevægelser zoomer; tommelfingerhjulet panorerer sidelæns.",
"ui.trim": "Trim",
"ui.twist": "Vridning",
"ui.two_finger_scroll_pans": "Rulning med to fingre panorerer",
@@ -2022,5 +2407,6 @@
"update.up_to_date": "SolidCraft er opdateret.",
"update.update_now": "Opdatér nu",
"update.you_have": "Du har {version}.",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk"
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ vælg · Enter anvender det indtastede · Esc lukker"
}
+13 -1
View File
@@ -338,7 +338,7 @@
"Profile / Edges": "Profil / Kanten",
"Profiles or curves": "Profile oder Kurven",
"Profiles/Curves": "Profile/Kurven",
"Quantity": "Größe",
"Quantity": "Anzahl",
"RECENT": "ZULETZT",
"REFERENCE": "REFERENZ",
"RULED SURFACE": "REGELFLÄCHE",
@@ -631,6 +631,7 @@
"cmd.document_metadata": "Konstruktionseigenschaften",
"cmd.document_metadata_remove": "Konstruktionseigenschaft entfernen",
"cmd.document_metadata_set": "Konstruktionseigenschaften ändern",
"cmd.document_revision": "Konstruktionsrevision",
"cmd.document_units": "Einheiten der Konstruktion ändern",
"cmd.edit_checkpoint": "Prüfpunkt",
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
@@ -1104,6 +1105,7 @@
"motion.key_here": "Schlüsselbild hier setzen",
"motion.key_here_hint": "Ein Schlüsselbild bei diesem Schritt mit dem aktuellen Gelenkwert hinzufügen",
"motion.no_collisions": "Keine Kollisionen in der Untersuchung",
"motion.plot_quantity": "Größe",
"motion.step_back": "Schritt zurück",
"motion.step_forward": "Schritt vor",
"motion.to_end": "Zum Ende",
@@ -1116,6 +1118,7 @@
"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.perspective.sketch": "Beim Bearbeiten einer Skizze wird die orthografische Ansicht verwendet. Beenden Sie die Skizze, um die Perspektive umzuschalten.",
"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.",
@@ -1641,6 +1644,7 @@
"ui.ai_http_busy": "der Endpunkt ist ausgelastet oder am Limit; versuchen Sie es später",
"ui.ai_http_key": "der Endpunkt hat den Schlüssel abgelehnt: prüfen Sie ihn unter Einstellungen > KI",
"ui.ai_http_not_found": "Endpunkt oder Modell nicht gefunden: prüfen Sie Basis-URL und Modellnamen",
"ui.ai_http_too_large": "die Unterhaltung ist für diesen Endpunkt zu groß; beginnen Sie einen neuen Chat",
"ui.ai_key": "API-Schlüssel",
"ui.ai_key_hint": "optional; im Schlüsselbund des Systems gespeichert",
"ui.ai_key_needs_https": "Ein Schlüssel wird nur über HTTPS oder an diesen Computer gesendet.",
@@ -1847,6 +1851,8 @@
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
"ui.findInTimeline": "In der Zeitleiste suchen",
"ui.findInWindow": "Im Fenster suchen",
"ui.fine": "Fein",
"ui.finest": "Sehr fein",
"ui.finish_sketch": "Skizze beenden",
"ui.fit": "Einpassen",
"ui.fix_unfix": "Fixieren/Lösen",
@@ -1935,6 +1941,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "WERKZEUGE",
"ui.heading_utility": "HILFSMITTEL",
"ui.heading_weldments": "SCHWEISSTEILE",
"ui.help": "Hilfe",
"ui.hide": "Ausblenden",
"ui.hideAxes": "Achsen ausblenden",
@@ -2029,6 +2036,7 @@
"ui.one_side": "Einseitig",
"ui.open": "Öffnen",
"ui.open_ends_tip": "Berührt nichts",
"ui.open_linked_design": "Verknüpftes Design öffnen",
"ui.open_preferences": "Einstellungen öffnen",
"ui.open_recent": "Zuletzt verwendet",
"ui.operation": "Operation",
@@ -2075,6 +2083,7 @@
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
"ui.properties": "Eigenschaften",
"ui.pull_direction": "Entformungsrichtung",
"ui.quality": "Qualität",
"ui.quantity": "Anzahl",
"ui.quantity_2": "Anzahl 2",
"ui.quit": "Beenden",
@@ -2176,6 +2185,7 @@
"ui.spline": "Spline",
"ui.split_body": "Körper trennen",
"ui.stand_up_tip": "Nach dem Öffnen einer STEP- oder IGES-Datei mit Z nach oben eine Vierteldrehung um X anbieten",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Von vorn beginnen",
"ui.start_page": "Startseite",
@@ -2229,6 +2239,7 @@
"ui.up_axis_note": "Welche Achse in den Ansichten nach oben zeigt (Achsenansicht, Home, Ansehen). Designs bleiben unverändert.",
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
"ui.view_centre": "Ansichtszentrum",
"ui.viewport_theme_follow": "Dem Design folgen",
"ui.visual_shaded_hidden_edges": "Schattiert mit verdeckten Kanten",
"ui.visual_shaded_with_edges": "Schattiert mit Kanten",
"ui.visual_style": "Ansichtsstil",
@@ -2278,6 +2289,7 @@
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
"update.prefs_label": "Updates",
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
"update.prefs_web": "Laden Sie die Seite neu, um die neueste Web-App zu erhalten. Ist das Netzwerk nicht verfügbar oder kann das Update nicht rechtzeitig vollständig heruntergeladen werden, wird die gespeicherte Version geöffnet. Ein laufender Download wird für das nächste Neuladen fortgesetzt.",
"update.release_page": "Release-Seite öffnen",
"update.restart": "Jetzt neu starten",
"update.restart_later": "Später",
+146 -2
View File
@@ -143,6 +143,7 @@
"Cube": "Cube",
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
"Current perspective view": "Current perspective view",
"Current plane": "Current plane",
"Curv. Comb": "Curv. Comb",
"Curvature (G2)": "Curvature (G2)",
"Curvature change (G3)": "Curvature change (G3)",
@@ -172,7 +173,12 @@
"Don't ask again": "Don't ask again",
"Double-click to rename": "Double-click to rename",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing",
"Drag to move the boss across the model. It snaps to sketch points on the face.": "Drag to move the boss across the model. It snaps to sketch points on the face.",
"Drag to move the hole; click to remove it": "Drag to move the hole; click to remove it",
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Drag to move the rest across the model. It snaps to sketch points on the face.",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Drag to move the snap fit across the model. It snaps to sketch points on the face.",
"Drag to point the catch in another direction.": "Drag to point the catch in another direction.",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.",
"Drive Joint": "Drive Joint",
"Duration": "Duration",
"Edges": "Edges",
@@ -239,7 +245,9 @@
"Gap": "Gap",
"Grid": "Grid",
"HISTORY": "HISTORY",
"Halo under sketch curves": "Halo under sketch curves",
"Height": "Height",
"Height From": "Height From",
"Hidden edges": "Hidden edges",
"History layout": "History layout",
"History names": "History names",
@@ -256,6 +264,7 @@
"In place (surfaces)": "In place (surfaces)",
"In plane": "In plane",
"Included": "Included",
"Inner Sharp": "Inner Sharp",
"Inside": "Inside",
"Instant": "Instant",
"Interface font": "Interface font",
@@ -302,6 +311,7 @@
"Mass Center": "Mass Center",
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
"Material": "Material",
"Material Outside": "Material Outside",
"Material copied. Existing body assignments are unchanged.": "Material copied. Existing body assignments are unchanged.",
"Max": "Max",
"Maximum": "Maximum",
@@ -314,7 +324,6 @@
"Mitre": "Mitre",
"Model Parameters": "Model Parameters",
"Modeled": "Modeled",
"More themes": "More themes",
"More toolbar panels": "More toolbar panels",
"Motion": "Motion",
"Mouse gestures": "Mouse gestures",
@@ -349,8 +358,10 @@
"On your computer": "On your computer",
"Opacity": "Opacity",
"Open GitHub draft": "Open GitHub draft",
"Open bodies (no volume)": "Open bodies (no volume)",
"Open library...": "Open library...",
"Open local libraries in the desktop app. You can export this design's library here.": "Open local libraries in the desktop app. You can export this design's library here.",
"Open surface — no volume": "Open surface — no volume",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Opens your browser and copies the report text for you to paste into GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Opens your browser. Review and submit your report on GitHub.",
@@ -359,6 +370,7 @@
"Other side": "Other side",
"Other surface (mutual trim)": "Other surface (mutual trim)",
"Other way": "Other way",
"Outer Sharp": "Outer Sharp",
"Outer radius": "Outer radius",
"Outside": "Outside",
"Overall length": "Overall length",
@@ -388,7 +400,6 @@
"Preview": "Preview",
"Preview Row": "Preview Row",
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
"Previous themes": "Previous themes",
"Profile / Edges": "Profile / Edges",
"Profiles or curves": "Profiles or curves",
"Profiles/Curves": "Profiles/Curves",
@@ -526,9 +537,11 @@
"Surface, or faces to turn over": "Surface, or faces to turn over",
"Swap Ends": "Swap Ends",
"TRIM": "TRIM",
"Tangent": "Tangent",
"Tangent (G1)": "Tangent (G1)",
"Tangent Chain": "Tangent Chain",
"Tangent chain": "Tangent chain",
"Tangent to Edge": "Tangent to Edge",
"Tap drill diameter": "Tap drill diameter",
"Target": "Target",
"Target Body": "Target Body",
@@ -684,6 +697,7 @@
"cmd.component_capture_position": "Capture Position",
"cmd.component_copies": "Find Identical Copies",
"cmd.component_create": "New Component",
"cmd.component_cycle_color": "Cycle Component Color",
"cmd.component_delete": "Delete Component",
"cmd.component_extract": "Extract to File",
"cmd.component_from_bodies": "Create Components from Bodies",
@@ -745,6 +759,7 @@
"cmd.document_metadata": "Design Properties",
"cmd.document_metadata_remove": "Remove Design Property",
"cmd.document_metadata_set": "Change Design Properties",
"cmd.document_revision": "Design Revision",
"cmd.document_units": "Change Design Units",
"cmd.edit_checkpoint": "Checkpoint",
"cmd.edit_restore_checkpoint": "Restore Checkpoint",
@@ -768,6 +783,8 @@
"cmd.inspect_accessibility": "Accessibility Analysis",
"cmd.inspect_center_of_mass": "Center of Mass",
"cmd.inspect_clearance": "Clearance",
"cmd.inspect_colored_components": "Enable Component Colors",
"cmd.inspect_colored_components_disable": "Disable Component Colors",
"cmd.inspect_compare": "Compare Bodies",
"cmd.inspect_continuity": "Continuity Analysis",
"cmd.inspect_curvature_comb": "Curvature Comb Analysis",
@@ -908,6 +925,7 @@
"cmd.sheet_refold": "Refold",
"cmd.sheet_rules": "List Sheet Metal Rules",
"cmd.sheet_unfold": "Unfold",
"cmd.sketch_arc3d": "3D Arc",
"cmd.sketch_arc_center_point": "Center Point Arc",
"cmd.sketch_arc_tangent": "Tangent Arc",
"cmd.sketch_arc_three_point": "3-Point Arc",
@@ -1030,17 +1048,25 @@
"cmd.view_back": "Back View",
"cmd.view_bottom": "Bottom View",
"cmd.view_delete": "Delete Named View",
"cmd.view_fit": "Fit",
"cmd.view_front": "Front View",
"cmd.view_graphics_preset": "Graphics Preset",
"cmd.view_grid": "Grid",
"cmd.view_ground_shadow": "Ground shadow",
"cmd.view_home": "Home view",
"cmd.view_isometric": "Isometric View",
"cmd.view_left": "Left View",
"cmd.view_list": "List Named Views",
"cmd.view_navigate": "Navigate View",
"cmd.view_navigation_bar": "Navigation bar",
"cmd.view_normal_to": "Normal to Selection",
"cmd.view_rename": "Rename Named View",
"cmd.view_right": "Right View",
"cmd.view_save": "New Named View",
"cmd.view_spin": "Continuous orbit",
"cmd.view_tool": "Navigation tool",
"cmd.view_top": "Top View",
"cmd.view_visual_style": "Visual style",
"color.copy": "Click to copy color values",
"color.edit": "Click to edit color",
"color.gamma": "Showing color values in 0-255 gamma space",
@@ -1081,6 +1107,7 @@
"crash.report": "Report…",
"crash.unclean_explanation": "Detected on a later launch; the exact exit time was not recorded. A system shutdown or forced stop can also cause an unclean exit.",
"delete.and_more": "… and {count} more",
"diagnostic.empty_boolean_component": "there is no body to cut or intersect in component '{component}'; the tool may overlap '{body}' in component '{other}' — activate that component or pick the body as a target",
"dialog.bodies_count": "{count} bodies",
"dialog.clearance_invalid": "the clearance must be a length of 0 or more",
"dialog.compute": "Compute",
@@ -1124,9 +1151,11 @@
"dxf.progress.write": "Writing DXF (5/5)",
"dxf.tolerance": "Curve tolerance",
"dxf.tolerance_help": "Visible bodies and shown sketches only. Curves use straight segments. Smaller tolerance gives finer curves and uses more time and memory.",
"file.open.drawing_filter": "Designs, CAD files, meshes and drawings",
"file_load.built": "Built so far: {built} of {total} features.",
"file_load.converting": "Converting solid {done} of {total}",
"file_load.inserting": "Inserting geometry",
"file_load.link_item": "{name} ({done} of {total})",
"file_load.opening": "Opening design",
"file_load.preparing_thumbnail": "Preparing the thumbnail...",
"file_load.preparing_view": "Preparing the view...",
@@ -1136,6 +1165,7 @@
"file_load.rebuilding": "Rebuilding feature {done} of {total}",
"file_load.recovering": "Recovering design",
"file_load.stopping": "Waiting for the previous operation to stop...",
"file_load.updating_links": "Updating links",
"file_load.waiting": "Waiting for file operation",
"fits the face, e.g. M8": "fits the face, e.g. M8",
"from the profile": "from the profile",
@@ -1163,6 +1193,26 @@
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
"help.solid.sweep": "Drags a sketch profile along a path to make a solid. Use it for pipes, rails, handles and any part of constant section that follows a curve.",
"help.third_party": "Third-party licences",
"hole_presets.confirm_delete": "Confirm delete",
"hole_presets.delete": "Delete",
"hole_presets.delete_prompt": "Delete this personal preset? Existing holes are unchanged.",
"hole_presets.full": "The personal library already contains 64 presets.",
"hole_presets.invalid_name": "Enter a short name without line breaks or control characters.",
"hole_presets.invalid_parameters": "Complete the hole parameters using valid expressions of reasonable length.",
"hole_presets.missing": "The selected preset is no longer available.",
"hole_presets.name": "Preset name",
"hole_presets.name_exists": "A preset with this name already exists. Use a different name.",
"hole_presets.name_hint": "A unique name",
"hole_presets.overwrite": "Save changes to selected preset",
"hole_presets.preset": "Preset",
"hole_presets.recall": "Recall",
"hole_presets.rename": "Rename",
"hole_presets.save": "Save new",
"hole_presets.scope": "Saved in your profile for all projects.",
"hole_presets.select": "Select a preset",
"hole_presets.title": "Personal presets",
"hole_presets.unsupported_options": "This preset uses thread options that this version cannot recall.",
"hole_presets.unsupported_overwrite": "This preset contains options that cannot be overwritten safely.",
"home.back": "Back to {name}",
"home.crashes": "Crash reports",
"home.design_workspace": "Design",
@@ -1238,6 +1288,8 @@
"joint.from_bodies_hint": "Each picked body moves into a new component of its own and stays where it is.",
"joint.one_body": "Both picks are on {body}. Pick the second one on another part.",
"joint.one_component": "These parts are bodies of one component: {bodies}. A joint moves one component against another, so each part needs a component of its own.",
"joint.rigid_group_one_component": "These {count} bodies are all in one component (or in the design itself): use {command} first.",
"joint.rigid_group_select_components": "Select two or more components.",
"keymap.command_id": "Command ID",
"keymap.empty": "No matching commands",
"keymap.use_anyway": "Use {key} here anyway",
@@ -1309,6 +1361,7 @@
"motion.key_here": "Key here",
"motion.key_here_hint": "Add a key at this step with the joint's value now",
"motion.no_collisions": "No collisions over the study",
"motion.plot_quantity": "Quantity",
"motion.step_back": "Step back",
"motion.step_forward": "Step on",
"motion.to_end": "To the end",
@@ -1321,6 +1374,7 @@
"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.perspective.sketch": "Sketch editing uses orthographic view. Finish the sketch to switch perspective.",
"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.",
@@ -1330,6 +1384,10 @@
"nav.touchpad.scroll": "Touchpad uses vertical scroll to zoom. Your two-finger pan setting is kept for other presets.",
"nav.touchpad.zoom": "Zoom (vertical scroll or pinch)",
"needs two joints": "needs two joints",
"network.allow_once": "Allow once",
"network.always_allow": "Always allow this server",
"network.prompt_detail": "This design links to a file on network location {host}. Opening it connects to that computer and may send your Windows sign-in to it. Only allow servers you trust.",
"network.prompt_title": "Network location",
"new configuration": "new configuration",
"new parameter": "new parameter",
"no hole": "no hole",
@@ -1358,6 +1416,7 @@
"pick the path": "pick the path",
"prefs.add_item": "Add the item",
"prefs.alt_frees_manipulators": "Alt / Option also frees manipulator arrows",
"prefs.anti_aliasing_amd_dx12": "Anti-aliasing uses 1× because the startup check drew nothing with multisampling and passed at 1×. You can choose another setting above; restart to apply it if the check passes.",
"prefs.anti_aliasing_note": "Smooths jagged edges.",
"prefs.applies_on_restart": "Applies on restart",
"prefs.assistant": "Assistant",
@@ -1401,6 +1460,10 @@
"prefs.gesture_pan": "Pan",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Graphics",
"prefs.graphics_card": "Graphics card",
"prefs.graphics_card_auto": "Automatic",
"prefs.graphics_card_missing": "The saved graphics card is unavailable; Automatic is used.",
"prefs.graphics_card_restart": "Takes effect after restarting SolidCraft. WGPU_ADAPTER_NAME overrides this choice.",
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note": "Presets set the choices below together.",
"prefs.history": "History",
@@ -1426,12 +1489,18 @@
"prefs.mouse_mapping": "Mouse mapping",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "How the mouse, trackpad and 3D mouse move the view.",
"prefs.network_host_remove": "Remove",
"prefs.network_locations": "Allow network locations in designs",
"prefs.network_locations_note": "Off: a design's linked files on another computer (\\\\server\\share) are opened only from the servers listed here, or after you allow one when asked.",
"prefs.network_locations_section": "Network locations",
"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.placement": "Placement",
"prefs.prefer_free_sketch_points": "Prefer movable sketch points when points overlap",
"prefs.preview_choices": "Preview of your choices",
"prefs.quality_default": "default",
"prefs.quality_fastest": "fastest",
@@ -1449,6 +1518,11 @@
"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_exit_view": "When a sketch ends",
"prefs.sketch_exit_view.keep": "Keep the current view",
"prefs.sketch_exit_view.previous": "Return to the previous view",
"prefs.sketch_exit_view.unless_rotated": "Return unless rotated",
"prefs.sketch_exit_view_note": "Where the view goes after Finish Sketch, Cancel or a double Esc.",
"prefs.sketch_selection": "Sketch & selection",
"prefs.sketch_selection_heading": "Sketch and selection",
"prefs.sketch_selection_note": "How sketches snap and how clicks select.",
@@ -1475,6 +1549,8 @@
"prefs.units_precision": "Units and precision",
"prefs.updates": "Updates",
"prefs.view": "View behavior",
"prefs.view_confirm_corner": "OK and Cancel in the view's corner",
"prefs.view_confirm_corner_note": "While a dialog or a sketch is open, a check and a cross at the top right of the view do what the dialog's OK and Cancel do, or finish and cancel the sketch. The view navigator moves down below them.",
"prefs.viewport": "Viewport",
"prefs.wheel": "Wheel",
"prefs.wheel_trackpad": "Wheel and trackpad",
@@ -2138,6 +2214,11 @@
"sketch_panel.author": "Author placement: {path}",
"sketch_panel.dimension_hint": "Enter a value or expression, then press OK.",
"sketch_panel.move_hint": "Enter distances and an angle, then press OK.",
"sketch_panel.plane_angle": "{plane}, turned by {angle}",
"sketch_panel.plane_fixed": "A fixed plane",
"sketch_panel.plane_flipped": "{plane}, from its other side",
"sketch_panel.plane_offset": "{plane}, offset by {distance}",
"sketch_panel.plane_origin": "The {plane} plane",
"sketch_panel.redefine_title": "{command}: {name}",
"sketch_panel.sheet_attachment": "Attached to a flat face of a sheet metal body; it follows that panel when the sheet changes.",
"sketch_panel.text_hint": "Edit the text and its placement, then press OK.",
@@ -2157,12 +2238,18 @@
"the parts library is not available": "the parts library is not available",
"thread.automatic_size": "Automatic size",
"thread.designation": "Designation",
"thread.direction": "Thread direction",
"thread.iso_metric": "ISO metric",
"thread.left_hand": "Left-hand",
"thread.right_hand": "Right-hand",
"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_folders_tip": "Keep their features and nested folders in the history",
"timeline.delete_many": "Delete {count} Selected Features",
"timeline.feature_error": "Error: {message}",
"timeline.feature_hint": "Double-click to edit · drag to reorder · right-click for more",
"timeline.feature_warning": "Warning: {message}",
"timeline.group": "New Feature Folder",
"timeline.move_many": "Move Timeline Items",
"timeline.move_to_group": "Move Features to Folder",
@@ -2312,6 +2399,7 @@
"ui.agent_chat_changes": "Changes: {n}, one undo step",
"ui.agent_chat_changes_n": "{n} changes",
"ui.agent_chat_changes_of": "{n}/{limit} changes",
"ui.agent_chat_choose_model": "Choose a model",
"ui.agent_chat_close": "Close",
"ui.agent_chat_command": "Agent command",
"ui.agent_chat_connect": "Connect",
@@ -2358,10 +2446,12 @@
"ui.agent_chat_key_in_env": "key set",
"ui.agent_chat_left": "Left",
"ui.agent_chat_limit": "The agent reached the limit of {limit} changes for one message. Send another message to let it go on.",
"ui.agent_chat_login_found": "login found",
"ui.agent_chat_minimize": "Minimise to a strip",
"ui.agent_chat_mode_all": "All",
"ui.agent_chat_mode_ask": "Ask",
"ui.agent_chat_mode_edits": "Edits",
"ui.agent_chat_model_no_tools": "{model} can't use SolidCraft's tools, so it can only chat. Pick a model with tool support.",
"ui.agent_chat_model_tip": "The model the agent uses. Your choice is remembered for this agent.",
"ui.agent_chat_new": "New chat",
"ui.agent_chat_new_confirm": "Start a new chat? The current one is cleared.",
@@ -2370,6 +2460,7 @@
"ui.agent_chat_no_control": "The agent chat's endpoint did not start, so the agent can't reach the design.",
"ui.agent_chat_no_images": "This agent doesn't take pictures",
"ui.agent_chat_no_picture": "There is no picture on the clipboard",
"ui.agent_chat_no_tool_call": "The model didn't use SolidCraft's tools, so the design wasn't changed. Choose a model that supports tool calling; see Local models in the agent chat guide.",
"ui.agent_chat_none_found": "No coding agent found. Install one of these, then press Refresh:",
"ui.agent_chat_none_found_short": "No agent found",
"ui.agent_chat_not_installed": "Not installed",
@@ -2387,6 +2478,9 @@
"ui.agent_chat_previous_picture": "Previous picture",
"ui.agent_chat_privacy": "Nothing goes anywhere except to the agent you run, under your own account.",
"ui.agent_chat_privacy_note": "SolidCraft sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_provider_models": "{count} models",
"ui.agent_chat_provider_other": "Other",
"ui.agent_chat_provider_tip": "The provider whose models the next list shows.",
"ui.agent_chat_queue": "Queue",
"ui.agent_chat_queue_full": "Too many messages are waiting: remove one or wait for the agent.",
"ui.agent_chat_queued": "Queued",
@@ -2455,6 +2549,7 @@
"ui.ai_http_busy": "the endpoint is busy or over its limit; try again later",
"ui.ai_http_key": "the endpoint refused the key: check it in Preferences > AI",
"ui.ai_http_not_found": "no such endpoint or model: check the base URL and the model name",
"ui.ai_http_too_large": "the conversation is too large for this endpoint; start a new chat",
"ui.ai_key": "API key",
"ui.ai_key_hint": "optional; kept in the system keychain",
"ui.ai_key_needs_https": "A key is only sent over HTTPS, or to this computer.",
@@ -2501,9 +2596,27 @@
"ui.ai_test_no_model": "Reached: {count} models, but not {model}",
"ui.ai_test_ok": "Reached: {count} models",
"ui.ai_testing": "Testing…",
"ui.ai_tool_arguments": "Arguments",
"ui.ai_tool_details": "Details",
"ui.ai_tool_done": "done",
"ui.ai_tool_failed": "failed",
"ui.ai_tool_result": "Result",
"ui.ai_tool_running": "running",
"ui.ai_tool_sum_bodies_one": "{count} body",
"ui.ai_tool_sum_bodies_other": "{count} bodies",
"ui.ai_tool_sum_by": "by {value}",
"ui.ai_tool_sum_edges_one": "{count} edge",
"ui.ai_tool_sum_edges_other": "{count} edges",
"ui.ai_tool_sum_faces_one": "{count} face",
"ui.ai_tool_sum_faces_other": "{count} faces",
"ui.ai_tool_sum_features_one": "{count} feature",
"ui.ai_tool_sum_features_other": "{count} features",
"ui.ai_tool_sum_measured": "Measured {what}",
"ui.ai_tool_sum_more": "{steps}, and {count} more",
"ui.ai_tool_sum_radius": "r {value}",
"ui.ai_tool_sum_saved": "Saved {file}",
"ui.ai_tool_sum_screenshot": "Screenshot",
"ui.ai_tool_sum_then": "{first}, then {next}",
"ui.ai_tool_unfold": "Click to show what it returned",
"ui.ai_tool_waiting": "waiting",
"ui.ai_toolbar_button": "Show Assistant button in application bar",
@@ -2594,6 +2707,8 @@
"ui.copy": "Copy",
"ui.corner": "Corner",
"ui.corners": "Corners",
"ui.cosmetic_thread_markings": "Cosmetic thread markings",
"ui.cosmetic_thread_markings_help": "Show schematic markings on threaded cylindrical walls. Changes only the display, never the design or exports.",
"ui.counterbore": "Counterbore",
"ui.countersink": "Countersink",
"ui.create": "Create",
@@ -2670,6 +2785,8 @@
"ui.filter_commands_or_keys": "Filter commands or keys",
"ui.findInTimeline": "Find in Timeline",
"ui.findInWindow": "Find in Window",
"ui.fine": "Fine",
"ui.finest": "Finest",
"ui.finish_sketch": "Finish Sketch",
"ui.fit": "Fit",
"ui.fix_unfix": "Fix/Unfix",
@@ -2759,6 +2876,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "UTILITIES",
"ui.heading_utility": "UTILITY",
"ui.heading_weldments": "WELDMENTS",
"ui.help": "Help",
"ui.hide": "Hide",
"ui.hideAxes": "Hide Axes",
@@ -2845,6 +2963,11 @@
"ui.move": "Move",
"ui.multiple": "Multiple",
"ui.named_views": "Named Views",
"ui.nav_bridge": "Navigate with a device app on this computer",
"ui.nav_bridge_connected": "Connected",
"ui.nav_bridge_port": "Port",
"ui.nav_bridge_tip": "A 3D input device's own program (for example a homemade 3D mouse's driver) moves the view over a local connection, so the mouse pointer stays yours. SolidCraft connects to it on this port on this computer only.",
"ui.nav_bridge_waiting": "Waiting for the device app",
"ui.navigation": "Navigation",
"ui.needs_design": "{command}: open a design first",
"ui.newSketchGroup": "New Sketch Group",
@@ -2863,6 +2986,7 @@
"ui.one_side": "One Side",
"ui.open": "Open",
"ui.open_ends_tip": "Touches nothing",
"ui.open_linked_design": "Open Linked Design",
"ui.open_preferences": "Open Preferences",
"ui.open_recent": "Open Recent",
"ui.operation": "Operation",
@@ -2882,18 +3006,27 @@
"ui.paste": "Paste",
"ui.path": "Path",
"ui.pattern_on_path": "Pattern on Path",
"ui.perforate_margin_default": "from the diameter",
"ui.perforate_pitch": "Pitch",
"ui.perforate_square": "Square",
"ui.perforate_staggered": "Staggered (60°)",
"ui.perforate_title": "PERFORATE",
"ui.perpendicular": "Perpendicular",
"ui.perspective_view": "Perspective view",
"ui.pinToShortcuts": "Pin to Shortcuts",
"ui.pinToToolbar": "Pin to Toolbar",
"ui.pipe": "Pipe",
"ui.placement": "Placement",
"ui.placement_handles": "Placement handles",
"ui.placement_handles_tip": "Handles on the preview of a Rest, Boss or Snap Fit: drag the marker to move it across the model, turn it by its ring",
"ui.plane": "Plane",
"ui.plane_at_angle": "Plane at Angle",
"ui.plugins": "Plugins",
"ui.plugins_desktop": "Plugins are available in the desktop app.",
"ui.plugins_empty": "No plugins installed.",
"ui.plugins_none": "No plugins match this search.",
"ui.plugins_note": "Each plugin is a folder inside {path} with a plugin.json. SolidCraft reads that file and does not run any code from the folder. Turning a plugin off keeps it installed.",
"ui.plugins_search": "Search plugins",
"ui.plugins_skipped": "{count} folder(s) could not be read.",
"ui.plugins_version": "{version}",
"ui.point": "Point",
@@ -2909,6 +3042,7 @@
"ui.project_face_edges_sketch": "Project face edges into new sketches",
"ui.properties": "Properties",
"ui.pull_direction": "Pull direction",
"ui.quality": "Quality",
"ui.quantity": "Quantity",
"ui.quantity_2": "Quantity 2",
"ui.quit": "Quit",
@@ -3015,6 +3149,7 @@
"ui.spline": "Spline",
"ui.split_body": "Split Body",
"ui.stand_up_tip": "After a STEP or IGES file opens with Z up, offer a quarter turn about X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Start over",
"ui.start_page": "Start page",
@@ -3054,6 +3189,7 @@
"ui.total_angle": "Total angle",
"ui.trackpad_pan_tip": "Two-finger scroll pans; in Fusion, Shift + two fingers orbits. Off: scroll zooms. Mouse wheel and pinch zoom; thumb wheel pans sideways.",
"ui.trim": "Trim",
"ui.turn_up_axis": "Turn Up Axis",
"ui.twist": "Twist",
"ui.two_finger_scroll_pans": "Two-finger scroll pans",
"ui.two_sides": "Two Sides",
@@ -3071,6 +3207,7 @@
"ui.updateToCurrentView": "Update to Current View",
"ui.vertical_constraint": "Vertical Constraint",
"ui.view_centre": "View centre",
"ui.viewport_theme_follow": "Follow theme",
"ui.visual_shaded_hidden_edges": "Shaded with hidden edges",
"ui.visual_shaded_with_edges": "Shaded with edges",
"ui.visual_style": "Visual style",
@@ -3124,6 +3261,7 @@
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software center. SolidCraft doesn't check.",
"update.prefs_label": "Updates",
"update.prefs_note": "This is SolidCraft {version} ({kind}). A check asks GitHub for the latest release and sends nothing else.",
"update.prefs_web": "Reload the page to get the latest web app. If the network is unavailable or the update cannot finish downloading in time, the saved version opens instead; a download in progress continues for the next reload.",
"update.release_page": "Open release page",
"update.restart": "Restart now",
"update.restart_later": "Later",
@@ -3134,6 +3272,12 @@
"update.up_to_date": "SolidCraft is up to date.",
"update.update_now": "Update now",
"update.you_have": "You have {version}.",
"view.auto_ortho_faces": "Perspective while orbiting, ortho on faces",
"web.files.insert_drawings_hint": "CAD files, meshes, DXF or SVG into the open design",
"web.files.insert_stored_drawings_hint": "Into the open design (CAD files, meshes, DXF, SVG)",
"web.open_full": "Open in SolidCraft",
"web.open_link": "Open linked design?",
"web.open_link_confirm": "There are unsaved changes. Open the linked design in a new tab?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ choose · Enter / Tab complete · Esc dismiss",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ choose · Enter applies what you typed · Esc dismiss"
}
+11
View File
@@ -1057,6 +1057,7 @@
"motion.key_here": "Insertar fotograma clave aquí",
"motion.key_here_hint": "Añadir un fotograma clave en este paso con el valor actual de la unión",
"motion.no_collisions": "Sin colisiones durante el estudio",
"motion.plot_quantity": "Cantidad",
"motion.step_back": "Paso atrás",
"motion.step_forward": "Paso adelante",
"motion.to_end": "Al final",
@@ -1758,6 +1759,9 @@
"thread.designation": "Designación",
"thread.iso_metric": "Métrica ISO",
"thread.select_size": "Seleccionar tamaño",
"timeline.delete_folders_tip": "Conservar sus operaciones y carpetas anidadas en el historial",
"timeline.feature_error": "Error: {message}",
"timeline.feature_warning": "Advertencia: {message}",
"timeline.group": "Nueva carpeta de operaciones",
"timeline.move_many": "Mover elementos de la línea de tiempo",
"timeline.move_to_group": "Mover operaciones a la carpeta",
@@ -2037,6 +2041,7 @@
"ui.ai_http_busy": "el servicio está ocupado o ha superado su límite; inténtalo más tarde",
"ui.ai_http_key": "el servicio ha rechazado la clave: compruébala en Preferencias > IA",
"ui.ai_http_not_found": "no existe ese servicio o modelo: comprueba la URL base y el nombre del modelo",
"ui.ai_http_too_large": "la conversación es demasiado grande para este endpoint; empieza un chat nuevo",
"ui.ai_key": "Clave de API",
"ui.ai_key_hint": "opcional; se guarda en el llavero del sistema",
"ui.ai_key_needs_https": "La clave solo se envía mediante HTTPS o a este ordenador.",
@@ -2240,6 +2245,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos o teclas",
"ui.findInTimeline": "Buscar en la línea de tiempo",
"ui.findInWindow": "Buscar en la ventana",
"ui.fine": "Fina",
"ui.finest": "Muy fina",
"ui.finish_sketch": "Finalizar boceto",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fijar/liberar",
@@ -2328,6 +2335,7 @@
"ui.heading_torus": "TORO",
"ui.heading_utilities": "UTILIDADES",
"ui.heading_utility": "UTILIDAD",
"ui.heading_weldments": "SOLDADURA",
"ui.help": "Ayuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar ejes",
@@ -2340,6 +2348,7 @@
"ui.history_first": "Ir al principio",
"ui.history_forward": "Avanzar un paso",
"ui.history_last": "Ir al final",
"ui.history_marker_tip": "Marcador del historial: arrastra para retroceder el modelo; las nuevas operaciones se insertan aquí",
"ui.history_title": "Historial",
"ui.hole": "Agujero",
"ui.hole_tap_type": "Tipo de roscado del agujero",
@@ -2464,6 +2473,7 @@
"ui.project_face_edges_sketch": "Proyectar aristas de caras en bocetos nuevos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de tracción",
"ui.quality": "Calidad",
"ui.quantity": "Cantidad",
"ui.quantity_2": "Cantidad 2",
"ui.quit": "Salir",
@@ -2558,6 +2568,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir cuerpo",
"ui.stand_up_tip": "Tras abrir un archivo STEP o IGES con Z hacia arriba, ofrecer un cuarto de vuelta alrededor de X",
"ui.standard": "Estándar",
"ui.start": "Iniciar",
"ui.start_over": "Volver a empezar",
"ui.start_page": "Página de inicio",
+8 -1
View File
@@ -270,7 +270,7 @@
"Preview Row": "Aperçu de la ligne",
"Preview leaves the current design unchanged.": "L'aperçu ne modifie pas la conception actuelle.",
"Profiles/Curves": "Profils/Courbes",
"Quantity": "Grandeur",
"Quantity": "Quantité",
"RECENT": "RÉCENTS",
"REFERENCE": "RÉFÉRENCE",
"RULED SURFACE": "SURFACE RÉGLÉE",
@@ -983,6 +983,7 @@
"motion.key_here": "Placer une image clé ici",
"motion.key_here_hint": "Ajouter une image clé à cette étape avec la valeur actuelle de la liaison",
"motion.no_collisions": "Aucune collision au cours de l'étude",
"motion.plot_quantity": "Grandeur",
"motion.step_back": "Étape précédente",
"motion.step_forward": "Étape suivante",
"motion.to_end": "À la fin",
@@ -1403,6 +1404,7 @@
"ui.ai_http_busy": "le point d'accès est occupé ou a dépassé sa limite ; réessayez plus tard",
"ui.ai_http_key": "le point d'accès a refusé la clé : vérifiez-la dans Préférences › IA",
"ui.ai_http_not_found": "point d'accès ou modèle introuvable : vérifiez l'URL de base et le nom du modèle",
"ui.ai_http_too_large": "la conversation est trop volumineuse pour ce point d'accès ; commencez une nouvelle discussion",
"ui.ai_key": "Clé d'API",
"ui.ai_key_hint": "facultative ; conservée dans le trousseau de clés du système",
"ui.ai_key_needs_https": "Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
@@ -1606,6 +1608,8 @@
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
"ui.findInTimeline": "Trouver dans la chronologie",
"ui.findInWindow": "Trouver dans la fenêtre",
"ui.fine": "Fine",
"ui.finest": "Très fine",
"ui.finish_sketch": "Terminer l'esquisse",
"ui.fit": "Cadrer",
"ui.fix_unfix": "Fixer/Libérer",
@@ -1694,6 +1698,7 @@
"ui.heading_torus": "TORE",
"ui.heading_utilities": "UTILITAIRES",
"ui.heading_utility": "UTILITAIRE",
"ui.heading_weldments": "MÉCANO-SOUDURE",
"ui.help": "Aide",
"ui.hide": "Masquer",
"ui.hideAxes": "Masquer les axes",
@@ -1830,6 +1835,7 @@
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
"ui.properties": "Propriétés",
"ui.pull_direction": "Direction de démoulage",
"ui.quality": "Qualité",
"ui.quantity": "Quantité",
"ui.quantity_2": "Quantité 2",
"ui.quit": "Quitter",
@@ -1924,6 +1930,7 @@
"ui.spline": "Spline",
"ui.split_body": "Scinder le corps",
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
"ui.standard": "Standard",
"ui.start": "Début",
"ui.start_over": "Recommencer",
"ui.start_page": "Page d’accueil",
+7
View File
@@ -971,6 +971,7 @@
"motion.key_here": "Inserir fotograma clave aquí",
"motion.key_here_hint": "Engadir un fotograma clave neste paso co valor actual da unión",
"motion.no_collisions": "Sen colisións durante o estudo",
"motion.plot_quantity": "Cantidade",
"motion.step_back": "Paso atrás",
"motion.step_forward": "Paso adiante",
"motion.to_end": "Ao final",
@@ -1389,6 +1390,7 @@
"ui.ai_http_busy": "o punto de acceso está ocupado ou superou o seu límite; téntao de novo máis tarde",
"ui.ai_http_key": "o punto de acceso rexeitou a clave: compróbaa en Preferencias > IA",
"ui.ai_http_not_found": "non existe ese punto de acceso ou modelo: comproba o URL base e o nome do modelo",
"ui.ai_http_too_large": "a conversa é demasiado grande para este punto de acceso; comeza un chat novo",
"ui.ai_key": "Clave da API",
"ui.ai_key_hint": "opcional; gárdase no chaveiro do sistema",
"ui.ai_key_needs_https": "A clave só se envía por HTTPS ou a este ordenador.",
@@ -1592,6 +1594,8 @@
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
"ui.findInTimeline": "Buscar na liña de tempo",
"ui.findInWindow": "Buscar na xanela",
"ui.fine": "Fina",
"ui.finest": "Moi fina",
"ui.finish_sketch": "Rematar o bosquexo",
"ui.fit": "Axustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1680,6 +1684,7 @@
"ui.heading_torus": "TORO",
"ui.heading_utilities": "UTILIDADES",
"ui.heading_utility": "UTILIDADE",
"ui.heading_weldments": "SOLDADURA",
"ui.help": "Axuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar os eixes",
@@ -1816,6 +1821,7 @@
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de extracción",
"ui.quality": "Calidade",
"ui.quantity": "Cantidade",
"ui.quantity_2": "Cantidade 2",
"ui.quit": "Saír",
@@ -1910,6 +1916,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir o corpo",
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
"ui.standard": "Estándar",
"ui.start": "Inicio",
"ui.start_over": "Volver comezar",
"ui.start_page": "Páxina de inicio",
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "Kulcs ide",
"motion.key_here_hint": "Kulcs hozzáadása ennél a lépésnél a csukló jelenlegi értékével",
"motion.no_collisions": "Nincs ütközés a vizsgálat során",
"motion.plot_quantity": "Mennyiség",
"motion.step_back": "Lépés vissza",
"motion.step_forward": "Lépés tovább",
"motion.to_end": "A végére",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "a végpont foglalt vagy túllépte a korlátját; próbálja később",
"ui.ai_http_key": "a végpont elutasította a kulcsot: ellenőrizze a Beállítások > AI menüben",
"ui.ai_http_not_found": "nincs ilyen végpont vagy modell: ellenőrizze az alap-URL-t és a modell nevét",
"ui.ai_http_too_large": "a beszélgetés túl nagy ehhez a végponthoz; kezdjen új csevegést",
"ui.ai_key": "API-kulcs",
"ui.ai_key_hint": "opcionális; a rendszer kulcstartójában tárolódik",
"ui.ai_key_needs_https": "A kulcs csak HTTPS-en vagy erre a számítógépre kerül elküldésre.",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
"ui.findInTimeline": "Keresés az idővonalon",
"ui.findInWindow": "Keresés az ablakban",
"ui.fine": "Finom",
"ui.finest": "Legfinomabb",
"ui.finish_sketch": "Vázlat befejezése",
"ui.fit": "Illesztés",
"ui.fix_unfix": "Rögzítés/feloldás",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "TÓRUSZ",
"ui.heading_utilities": "SEGÉDESZKÖZÖK",
"ui.heading_utility": "SEGÉDESZKÖZ",
"ui.heading_weldments": "HEGESZTÉS",
"ui.help": "Súgó",
"ui.hide": "Elrejtés",
"ui.hideAxes": "Tengelyek elrejtése",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
"ui.properties": "Tulajdonságok",
"ui.pull_direction": "Húzási irány",
"ui.quality": "Minőség",
"ui.quantity": "Mennyiség",
"ui.quantity_2": "2. mennyiség",
"ui.quit": "Kilépés",
@@ -1852,6 +1858,7 @@
"ui.spline": "Spline",
"ui.split_body": "Test felosztása",
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
"ui.standard": "Normál",
"ui.start": "Kezdés",
"ui.start_over": "Újrakezdés",
"ui.start_page": "Kezdőoldal",
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "Key di sini",
"motion.key_here_hint": "Tambah key pada langkah ini dengan nilai sambungan saat ini",
"motion.no_collisions": "Tidak ada tabrakan sepanjang studi",
"motion.plot_quantity": "Jumlah",
"motion.step_back": "Mundur satu langkah",
"motion.step_forward": "Maju satu langkah",
"motion.to_end": "Ke akhir",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "endpoint sibuk atau melebihi batas; coba lagi nanti",
"ui.ai_http_key": "endpoint menolak key: periksa di Preferensi > AI",
"ui.ai_http_not_found": "endpoint atau model tidak ada: periksa URL dasar dan nama model",
"ui.ai_http_too_large": "percakapan terlalu besar untuk endpoint ini; mulai obrolan baru",
"ui.ai_key": "Key API",
"ui.ai_key_hint": "opsional; disimpan di keychain sistem",
"ui.ai_key_needs_https": "Key hanya dikirim lewat HTTPS, atau ke komputer ini.",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
"ui.findInTimeline": "Cari di Linimasa",
"ui.findInWindow": "Cari di Jendela",
"ui.fine": "Halus",
"ui.finest": "Sangat halus",
"ui.finish_sketch": "Selesai Sketsa",
"ui.fit": "Pas",
"ui.fix_unfix": "Kunci/Lepas Kunci",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "UTILITAS",
"ui.heading_utility": "UTILITAS",
"ui.heading_weldments": "KONSTRUKSI LAS",
"ui.help": "Bantuan",
"ui.hide": "Sembunyikan",
"ui.hideAxes": "Sembunyikan Sumbu",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
"ui.properties": "Properti",
"ui.pull_direction": "Arah tarik",
"ui.quality": "Kualitas",
"ui.quantity": "Jumlah",
"ui.quantity_2": "Jumlah 2",
"ui.quit": "Keluar",
@@ -1852,6 +1858,7 @@
"ui.spline": "Spline",
"ui.split_body": "Belah Bodi",
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
"ui.standard": "Standar",
"ui.start": "Mulai",
"ui.start_over": "Mulai ulang",
"ui.start_page": "Halaman awal",
+30
View File
@@ -873,6 +873,26 @@
"help.mit": "Licenza MIT",
"help.notice": "NOTICE",
"help.third_party": "Licenze di terze parti",
"hole_presets.confirm_delete": "Conferma eliminazione",
"hole_presets.delete": "Elimina",
"hole_presets.delete_prompt": "Eliminare questo preset personale? I fori esistenti restano invariati.",
"hole_presets.full": "La raccolta personale contiene già 64 preset.",
"hole_presets.invalid_name": "Inserisci un nome breve, senza a capo o caratteri di controllo.",
"hole_presets.invalid_parameters": "Completa i parametri del foro con espressioni valide e di lunghezza ragionevole.",
"hole_presets.missing": "Il preset selezionato non è più disponibile.",
"hole_presets.name": "Nome preset",
"hole_presets.name_exists": "Esiste già un preset con questo nome. Usa un nome diverso.",
"hole_presets.name_hint": "Un nome univoco",
"hole_presets.overwrite": "Salva le modifiche nel preset selezionato",
"hole_presets.preset": "Preset",
"hole_presets.recall": "Richiama",
"hole_presets.rename": "Rinomina",
"hole_presets.save": "Salva nuovo",
"hole_presets.scope": "Salvati nel tuo profilo per tutti i progetti.",
"hole_presets.select": "Seleziona un preset",
"hole_presets.title": "Preset personali",
"hole_presets.unsupported_options": "Questo preset usa opzioni del filetto che questa versione non può richiamare.",
"hole_presets.unsupported_overwrite": "Questo preset contiene opzioni che non possono essere sovrascritte in sicurezza.",
"home.back": "Torna a {name}",
"home.crashes": "Rapporti di arresto anomalo",
"home.design_workspace": "Progettazione",
@@ -976,6 +996,7 @@
"motion.key_here": "Inserisci fotogramma chiave qui",
"motion.key_here_hint": "Aggiungi un fotogramma chiave in questo passo con il valore attuale del giunto",
"motion.no_collisions": "Nessuna collisione durante lo studio",
"motion.plot_quantity": "Quantità",
"motion.step_back": "Passo indietro",
"motion.step_forward": "Passo avanti",
"motion.to_end": "Alla fine",
@@ -1130,7 +1151,10 @@
"the parts library is not available": "la libreria delle parti non è disponibile",
"thread.automatic_size": "Dimensione automatica",
"thread.designation": "Designazione",
"thread.direction": "Verso del filetto",
"thread.iso_metric": "Metrica ISO",
"thread.left_hand": "Sinistro",
"thread.right_hand": "Destro",
"thread.select_size": "Seleziona dimensione",
"timeline.group": "Nuova cartella di lavorazioni",
"timeline.move_many": "Sposta elementi della sequenza temporale",
@@ -1405,6 +1429,7 @@
"ui.ai_http_busy": "l'endpoint è occupato o ha superato il limite; riprova più tardi",
"ui.ai_http_key": "l'endpoint ha rifiutato la chiave: controllala in Preferenze > IA",
"ui.ai_http_not_found": "endpoint o modello inesistente: controlla l'URL di base e il nome del modello",
"ui.ai_http_too_large": "la conversazione è troppo grande per questo endpoint; inizia una nuova chat",
"ui.ai_key": "Chiave API",
"ui.ai_key_hint": "facoltativa; conservata nel portachiavi di sistema",
"ui.ai_key_needs_https": "La chiave viene inviata solo tramite HTTPS o a questo computer.",
@@ -1608,6 +1633,8 @@
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
"ui.findInTimeline": "Trova nella sequenza temporale",
"ui.findInWindow": "Trova nella finestra",
"ui.fine": "Fine",
"ui.finest": "Finissima",
"ui.finish_sketch": "Termina schizzo",
"ui.fit": "Adatta",
"ui.fix_unfix": "Fissa/Libera",
@@ -1697,6 +1724,7 @@
"ui.heading_torus": "TORO",
"ui.heading_utilities": "UTILITÀ",
"ui.heading_utility": "UTILITÀ",
"ui.heading_weldments": "SALDATURE",
"ui.help": "Aiuto",
"ui.hide": "Nascondi",
"ui.hideAxes": "Nascondi assi",
@@ -1833,6 +1861,7 @@
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
"ui.properties": "Proprietà",
"ui.pull_direction": "Direzione di estrazione",
"ui.quality": "Qualità",
"ui.quantity": "Quantità",
"ui.quantity_2": "Quantità 2",
"ui.quit": "Esci",
@@ -1927,6 +1956,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividi corpo",
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
"ui.standard": "Standard",
"ui.start": "Inizio",
"ui.start_over": "Ricomincia",
"ui.start_page": "Pagina iniziale",
+147 -3
View File
@@ -143,6 +143,7 @@
"Cube": "キューブ",
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
"Current perspective view": "現在の透視投影",
"Current plane": "現在の平面",
"Curv. Comb": "曲率コーム",
"Curvature (G2)": "曲率 (G2)",
"Curvature change (G3)": "曲率変化 (G3)",
@@ -172,7 +173,12 @@
"Don't ask again": "今後確認しない",
"Double-click to rename": "ダブルクリックで名前変更",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "ビュー上で中ボタンをダブルクリックした時の動作: ホーム(全体を収めたホームビュー)、全体表示(向きを維持して全体を収める)、または無効",
"Drag to move the boss across the model. It snaps to sketch points on the face.": "ドラッグしてボスをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to move the hole; click to remove it": "ドラッグで穴を移動、クリックで削除",
"Drag to move the rest across the model. It snaps to sketch points on the face.": "ドラッグしてレストをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "ドラッグしてスナップ フィットをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to point the catch in another direction.": "ドラッグしてキャッチの向きを変えます。",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "ドラッグしてレストを 5° 刻みで回転します。Alt または Ctrl を押したままにすると自由に回転します。",
"Drive Joint": "ジョイントを駆動",
"Duration": "時間",
"Edges": "エッジ",
@@ -239,7 +245,9 @@
"Gap": "間隔",
"Grid": "グリッド",
"HISTORY": "履歴",
"Halo under sketch curves": "スケッチ曲線の下に縁取りを表示",
"Height": "高さ",
"Height From": "高さの基準",
"Hidden edges": "隠線",
"History layout": "履歴のレイアウト",
"History names": "履歴名",
@@ -256,6 +264,7 @@
"In place (surfaces)": "その場で(サーフェス)",
"In plane": "平面内",
"Included": "挟角",
"Inner Sharp": "内側仮想シャープ",
"Inside": "内側",
"Instant": "即時",
"Interface font": "インターフェースフォント",
@@ -302,6 +311,7 @@
"Mass Center": "重心",
"Matched restrictions check the final values.": "条件に一致する制限ルールが最終値を確認します。",
"Material": "材料",
"Material Outside": "材料外側",
"Material copied. Existing body assignments are unchanged.": "マテリアルをコピーしました。既存のボディの割り当ては変わりません。",
"Max": "最大",
"Maximum": "最大",
@@ -314,7 +324,6 @@
"Mitre": "留め継ぎ",
"Model Parameters": "モデルパラメータ",
"Modeled": "モデル化済み",
"More themes": "その他のテーマ",
"More toolbar panels": "その他のツールバーパネル",
"Motion": "モーション",
"Mouse gestures": "マウス ジェスチャ",
@@ -349,8 +358,10 @@
"On your computer": "このコンピューター上",
"Opacity": "不透明度",
"Open GitHub draft": "GitHubの下書きを開く",
"Open bodies (no volume)": "開いたボディ(体積なし)",
"Open library...": "ライブラリを開く...",
"Open local libraries in the desktop app. You can export this design's library here.": "ローカルライブラリはデスクトップアプリで開いてください。このデザインのライブラリはここで書き出せます。",
"Open surface — no volume": "開いたサーフェス — 体積なし",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
"Opens your browser and copies the report text for you to paste into GitHub.": "ブラウザーを開き、GitHubに貼り付ける報告本文をコピーします。",
"Opens your browser. Review and submit your report on GitHub.": "ブラウザーを開きます。GitHubで内容を確認して投稿してください。",
@@ -359,6 +370,7 @@
"Other side": "反対側",
"Other surface (mutual trim)": "相手のサーフェス(相互トリム)",
"Other way": "反対方向",
"Outer Sharp": "外側仮想シャープ",
"Outer radius": "外側コーナー半径",
"Outside": "外側",
"Overall length": "全長",
@@ -388,11 +400,10 @@
"Preview": "プレビュー",
"Preview Row": "行をプレビュー",
"Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。",
"Previous themes": "前のテーマ",
"Profile / Edges": "プロファイル / エッジ",
"Profiles or curves": "プロファイルまたは曲線",
"Profiles/Curves": "プロファイル/曲線",
"Quantity": "量",
"Quantity": "数量",
"RECENT": "最近使用した項目",
"REFERENCE": "参照",
"RULED SURFACE": "ルールドサーフェス",
@@ -526,9 +537,11 @@
"Surface, or faces to turn over": "サーフェス、または裏返す面",
"Swap Ends": "端点を入れ替え",
"TRIM": "トリム",
"Tangent": "正接",
"Tangent (G1)": "接線 (G1)",
"Tangent Chain": "正接チェーン",
"Tangent chain": "接線チェーン",
"Tangent to Edge": "エッジに正接",
"Tap drill diameter": "タップ下穴径",
"Target": "ターゲット",
"Target Body": "ターゲットボディ",
@@ -684,6 +697,7 @@
"cmd.component_capture_position": "位置をキャプチャ",
"cmd.component_copies": "同一のコピーを検索",
"cmd.component_create": "新規コンポーネント",
"cmd.component_cycle_color": "コンポーネントの色を順に変更",
"cmd.component_delete": "コンポーネントを削除",
"cmd.component_extract": "ファイルに抽出",
"cmd.component_from_bodies": "ボディからコンポーネントを作成",
@@ -745,6 +759,7 @@
"cmd.document_metadata": "デザインのプロパティ",
"cmd.document_metadata_remove": "デザインのプロパティを削除",
"cmd.document_metadata_set": "デザインのプロパティを変更",
"cmd.document_revision": "デザイン リビジョン",
"cmd.document_units": "デザインの単位を変更",
"cmd.edit_checkpoint": "チェックポイント",
"cmd.edit_restore_checkpoint": "チェックポイントを復元",
@@ -768,6 +783,8 @@
"cmd.inspect_accessibility": "アクセシビリティ解析",
"cmd.inspect_center_of_mass": "重心",
"cmd.inspect_clearance": "クリアランス",
"cmd.inspect_colored_components": "コンポーネントの色分けを有効化",
"cmd.inspect_colored_components_disable": "コンポーネントの色分けを無効化",
"cmd.inspect_compare": "ボディを比較",
"cmd.inspect_continuity": "連続性解析",
"cmd.inspect_curvature_comb": "曲率コーム解析",
@@ -908,6 +925,7 @@
"cmd.sheet_refold": "折り戻し",
"cmd.sheet_rules": "板金ルールを一覧表示",
"cmd.sheet_unfold": "展開",
"cmd.sketch_arc3d": "3D 円弧",
"cmd.sketch_arc_center_point": "中心点円弧",
"cmd.sketch_arc_tangent": "正接円弧",
"cmd.sketch_arc_three_point": "3点指定円弧",
@@ -1030,17 +1048,25 @@
"cmd.view_back": "背面ビュー",
"cmd.view_bottom": "下面ビュー",
"cmd.view_delete": "名前付きビューを削除",
"cmd.view_fit": "フィット",
"cmd.view_front": "正面ビュー",
"cmd.view_graphics_preset": "グラフィックスプリセット",
"cmd.view_grid": "グリッド",
"cmd.view_ground_shadow": "地面の影",
"cmd.view_home": "ホームビュー",
"cmd.view_isometric": "等角ビュー",
"cmd.view_left": "左側面ビュー",
"cmd.view_list": "名前付きビューを一覧表示",
"cmd.view_navigate": "ビューナビゲーション",
"cmd.view_navigation_bar": "ナビゲーションバー",
"cmd.view_normal_to": "選択アイテムに垂直",
"cmd.view_rename": "名前付きビューの名前を変更",
"cmd.view_right": "右側面ビュー",
"cmd.view_save": "新規名前付きビュー",
"cmd.view_spin": "連続オービット",
"cmd.view_tool": "ナビゲーションツール",
"cmd.view_top": "上面ビュー",
"cmd.view_visual_style": "表示スタイル",
"color.copy": "クリックして色の値をコピー",
"color.edit": "クリックして色を編集",
"color.gamma": "0〜255のガンマ空間で色の値を表示",
@@ -1081,6 +1107,7 @@
"crash.report": "報告…",
"crash.unclean_explanation": "次回以降の起動時に検出されました。正確な終了時刻は記録されていません。システムのシャットダウンや強制停止によっても、正常終了が記録されない場合があります。",
"delete.and_more": "…ほか{count}件",
"diagnostic.empty_boolean_component": "コンポーネント '{component}' に切り取りまたは交差の対象となるボディがありません。ツールはコンポーネント '{other}' の '{body}' と重なる可能性があります。そのコンポーネントをアクティブにするか、ボディを対象として選択してください",
"dialog.bodies_count": "{count} ボディ",
"dialog.clearance_invalid": "クリアランスは 0 以上の長さにしてください",
"dialog.compute": "計算",
@@ -1124,9 +1151,11 @@
"dxf.progress.write": "DXFを書き込み中 (5/5)",
"dxf.tolerance": "曲線の許容誤差",
"dxf.tolerance_help": "表示中のボディとスケッチを出力し、曲線は線分で近似します。許容誤差を小さくすると曲線が滑らかになりますが、処理時間とメモリ使用量が増えます。",
"file.open.drawing_filter": "デザイン、CADファイル、メッシュ、図面",
"file_load.built": "構築済み: 全{total}フィーチャー中{built}件。",
"file_load.converting": "ソリッドを変換中: {done}/{total}",
"file_load.inserting": "ジオメトリを挿入中",
"file_load.link_item": "{name} ({done}/{total})",
"file_load.opening": "デザインを開いています",
"file_load.preparing_thumbnail": "サムネイルを準備中...",
"file_load.preparing_view": "ビューを準備中...",
@@ -1136,6 +1165,7 @@
"file_load.rebuilding": "フィーチャーを再構築中: {done}/{total}",
"file_load.recovering": "デザインを復元中",
"file_load.stopping": "前の操作の停止を待っています...",
"file_load.updating_links": "リンクを更新中",
"file_load.waiting": "ファイル操作を待っています",
"fits the face, e.g. M8": "面に適合するサイズ (例: M8)",
"from the profile": "形材から",
@@ -1163,6 +1193,26 @@
"help.solid.shell": "立体の中をくり抜き、指定した厚さの壁を残します。選んだ面を開口にすることもできます。箱、カバーなどの薄肉の部品に使います。",
"help.solid.sweep": "スケッチの輪郭をパスに沿って動かし、立体を作ります。パイプ、レール、ハンドルなど、一定の断面で曲線をたどる部品に使います。",
"help.third_party": "サードパーティライセンス",
"hole_presets.confirm_delete": "削除を確認",
"hole_presets.delete": "削除",
"hole_presets.delete_prompt": "この個人用プリセットを削除しますか?既存の穴は変更されません。",
"hole_presets.full": "個人用ライブラリには既に64個のプリセットがあります。",
"hole_presets.invalid_name": "改行や制御文字を含まない短い名前を入力してください。",
"hole_presets.invalid_parameters": "適切な長さの有効な式で穴のパラメーターを入力してください。",
"hole_presets.missing": "選択したプリセットは利用できなくなりました。",
"hole_presets.name": "プリセット名",
"hole_presets.name_exists": "同じ名前のプリセットが既にあります。別の名前を指定してください。",
"hole_presets.name_hint": "一意の名前",
"hole_presets.overwrite": "選択したプリセットの変更を保存",
"hole_presets.preset": "プリセット",
"hole_presets.recall": "呼び出し",
"hole_presets.rename": "名前を変更",
"hole_presets.save": "新規保存",
"hole_presets.scope": "すべてのプロジェクトで使えるようにプロファイルに保存します。",
"hole_presets.select": "プリセットを選択",
"hole_presets.title": "個人用プリセット",
"hole_presets.unsupported_options": "このプリセットには、このバージョンで呼び出せないねじオプションが含まれています。",
"hole_presets.unsupported_overwrite": "このプリセットには安全に上書きできないオプションが含まれています。",
"home.back": "{name} に戻る",
"home.crashes": "クラッシュレポート",
"home.design_workspace": "デザイン",
@@ -1238,6 +1288,8 @@
"joint.from_bodies_hint": "選択した各ボディを、位置はそのままで専用の新しいコンポーネントに移動します。",
"joint.one_body": "両方の選択が {body} 上にあります。2 つ目は別の部品で選択してください。",
"joint.one_component": "これらの部品は 1 つのコンポーネントのボディです: {bodies}。ジョイントはコンポーネント同士を動かすため、部品ごとに専用のコンポーネントが必要です。",
"joint.rigid_group_one_component": "これらの {count} 個のボディは、すべて 1 つのコンポーネント内(またはデザイン自体)にあります。先に「{command}」を使用してください。",
"joint.rigid_group_select_components": "2 つ以上のコンポーネントを選択してください。",
"keymap.command_id": "コマンドID",
"keymap.empty": "一致するコマンドはありません",
"keymap.use_anyway": "ここでは{key}をそのまま使用",
@@ -1309,6 +1361,7 @@
"motion.key_here": "ここにキーフレームを追加",
"motion.key_here_hint": "このステップにジョイントの現在値でキーフレームを追加",
"motion.no_collisions": "スタディ全体で衝突はありません",
"motion.plot_quantity": "量",
"motion.step_back": "前のステップ",
"motion.step_forward": "次のステップ",
"motion.to_end": "最後へ",
@@ -1321,6 +1374,7 @@
"nav.fusion.help": "中ドラッグでパン、Shift+中ドラッグで回転、右ドラッグで回転。",
"nav.inventor.help": "中ドラッグでパン、Shift+中ドラッグで回転、Ctrl+中ドラッグでズーム、右ドラッグで回転。",
"nav.openscad.help": "空きスペースで左ドラッグすると回転、Shift+左ドラッグで矩形選択、スケッチ ジオメトリはドラッグで編集。右または中ドラッグでパン、Shift+右または Shift+中ドラッグでズーム。",
"nav.perspective.sketch": "スケッチ編集中は平行投影で表示されます。透視投影に切り替えるにはスケッチを終了してください。",
"nav.solidworks.help": "中ドラッグで回転、Ctrl+中ドラッグでパン、Shift+中ドラッグでズーム、右ドラッグで回転。",
"nav.tinkercad.help": "右ドラッグで回転、中ドラッグまたは Shift+右ドラッグでパン。",
"nav.touchpad.help": "Shift を押しながらポインターを動かすとパン、Alt/Option を押しながら動かすと回転します。マウスボタンは不要です。縦スクロールでズームします。",
@@ -1330,6 +1384,10 @@
"nav.touchpad.scroll": "タッチパッドでは縦スクロールでズームします。2 本指パンの設定は他のプリセット用に保持されます。",
"nav.touchpad.zoom": "ズーム(縦スクロールまたはピンチ)",
"needs two joints": "2つのジョイントが必要",
"network.allow_once": "今回のみ許可",
"network.always_allow": "このサーバーを常に許可",
"network.prompt_detail": "このデザインはネットワーク上の場所 {host} にあるファイルにリンクしています。開くとそのコンピューターに接続し、Windows のサインイン情報が送信される場合があります。信頼できるサーバーだけを許可してください。",
"network.prompt_title": "ネットワーク上の場所",
"new configuration": "新規構成",
"new parameter": "新規パラメータ",
"no hole": "穴なし",
@@ -1358,6 +1416,7 @@
"pick the path": "パスを選択",
"prefs.add_item": "項目を追加",
"prefs.alt_frees_manipulators": "Alt / Option でマニピュレーターの矢印も自由に動かす",
"prefs.anti_aliasing_amd_dx12": "起動時のチェックでマルチサンプリングでは何も描画されず、1×では正常に描画されたため、アンチエイリアシングを1×に設定しました。上で別の設定を選択できます。再起動後のチェックに合格すると、その設定が適用されます。",
"prefs.anti_aliasing_note": "輪郭のぎざぎざを滑らかにします。",
"prefs.applies_on_restart": "再起動後に適用",
"prefs.assistant": "アシスタント",
@@ -1401,6 +1460,10 @@
"prefs.gesture_pan": "移動",
"prefs.gesture_zoom": "ズーム",
"prefs.graphics": "グラフィックス",
"prefs.graphics_card": "グラフィックスカード",
"prefs.graphics_card_auto": "自動",
"prefs.graphics_card_missing": "保存されたグラフィックスカードが利用できないため、自動選択を使用します。",
"prefs.graphics_card_restart": "SolidCraft の再起動後に有効になります。WGPU_ADAPTER_NAME がこの選択より優先されます。",
"prefs.graphics_note": "速度と滑らかさのバランスを調整します。各選択肢の効果をサンプルで確認できます。",
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
"prefs.history": "履歴",
@@ -1426,12 +1489,18 @@
"prefs.mouse_mapping": "マウスの割り当て",
"prefs.navigation": "ナビゲーション",
"prefs.navigation_note": "マウス、トラックパッド、3Dマウスによるビューの操作。",
"prefs.network_host_remove": "削除",
"prefs.network_locations": "デザイン内のネットワーク上の場所を許可",
"prefs.network_locations_note": "オフ: 別のコンピューター (\\\\server\\share) にあるデザインのリンク先ファイルは、ここに表示されたサーバー、または確認時に許可したサーバーからのみ開きます。",
"prefs.network_locations_section": "ネットワーク上の場所",
"prefs.not_on_gpu": "この GPU は非対応",
"prefs.on": "オン",
"prefs.opaque": "不透明",
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
"prefs.orientation": "方向",
"prefs.overall_speed": "全体の速度",
"prefs.placement": "配置",
"prefs.prefer_free_sketch_points": "スケッチ点が重なったときは移動可能な点を優先する",
"prefs.preview_choices": "選択した設定のプレビュー",
"prefs.quality_default": "既定",
"prefs.quality_fastest": "最速",
@@ -1449,6 +1518,11 @@
"prefs.shift_add_ctrl_toggle": "Shift は追加のみ、Ctrl / Cmd は選択を切り替え",
"prefs.shift_add_note": "オン:Shift は追加、Ctrl / Cmd は選択を切り替えます。オフ(既定):どちらも選択を切り替えます。",
"prefs.shortcuts": "ショートカット",
"prefs.sketch_exit_view": "スケッチ終了時のビュー",
"prefs.sketch_exit_view.keep": "現在のビューを維持",
"prefs.sketch_exit_view.previous": "前のビューに戻る",
"prefs.sketch_exit_view.unless_rotated": "回転していなければ戻る",
"prefs.sketch_exit_view_note": "「スケッチを終了」、キャンセル、Esc の 2 回押しの後にビューが移る先です。",
"prefs.sketch_selection": "スケッチと選択",
"prefs.sketch_selection_heading": "スケッチと選択",
"prefs.sketch_selection_note": "スケッチのスナップとクリックによる選択。",
@@ -1475,6 +1549,8 @@
"prefs.units_precision": "単位と精度",
"prefs.updates": "更新",
"prefs.view": "ビューの動作",
"prefs.view_confirm_corner": "ビューの隅に OK とキャンセル",
"prefs.view_confirm_corner_note": "ダイアログまたはスケッチを開いている間、ビュー右上のチェックとバツが、ダイアログの OK とキャンセルと同じ操作を行うか、スケッチを終了またはキャンセルします。ビューナビゲーターはその下に移動します。",
"prefs.viewport": "ビューポート",
"prefs.wheel": "ホイール",
"prefs.wheel_trackpad": "ホイールとトラックパッド",
@@ -2138,6 +2214,11 @@
"sketch_panel.author": "作成時の配置: {path}",
"sketch_panel.dimension_hint": "値または式を入力し、OK を押してください。",
"sketch_panel.move_hint": "距離と角度を入力し、OK を押してください。",
"sketch_panel.plane_angle": "{plane}、{angle} 回転",
"sketch_panel.plane_fixed": "固定平面",
"sketch_panel.plane_flipped": "{plane}(裏側から)",
"sketch_panel.plane_offset": "{plane}、{distance} オフセット",
"sketch_panel.plane_origin": "{plane} 平面",
"sketch_panel.redefine_title": "{command}:{name}",
"sketch_panel.sheet_attachment": "板金ボディの平面に接続されています。板金が変更されると、そのパネルに追従します。",
"sketch_panel.text_hint": "テキストと配置を編集し、OK を押してください。",
@@ -2157,12 +2238,18 @@
"the parts library is not available": "パーツライブラリは利用できません",
"thread.automatic_size": "サイズを自動選択",
"thread.designation": "ねじの呼び",
"thread.direction": "ねじの方向",
"thread.iso_metric": "ISOメートルねじ",
"thread.left_hand": "左ねじ",
"thread.right_hand": "右ねじ",
"thread.select_size": "サイズを選択",
"timeline.delete_after_marker_one": "履歴マーカー以降の 1 個のフィーチャーを削除",
"timeline.delete_after_marker_other": "履歴マーカー以降の {count} 個のフィーチャーを削除",
"timeline.delete_folders_tip": "フィーチャーと入れ子のフォルダーを履歴に残します",
"timeline.delete_many": "選択した {count} 個のフィーチャーを削除",
"timeline.feature_error": "エラー: {message}",
"timeline.feature_hint": "ダブルクリックで編集 · ドラッグで並べ替え · 右クリックでその他の操作",
"timeline.feature_warning": "警告: {message}",
"timeline.group": "フィーチャーフォルダを新規作成",
"timeline.move_many": "タイムライン項目を移動",
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
@@ -2312,6 +2399,7 @@
"ui.agent_chat_changes": "{n} 件の変更(元に戻すは1回)",
"ui.agent_chat_changes_n": "{n} 件の変更",
"ui.agent_chat_changes_of": "変更 {n}/{limit}",
"ui.agent_chat_choose_model": "モデルを選択",
"ui.agent_chat_close": "閉じる",
"ui.agent_chat_command": "エージェントのコマンド",
"ui.agent_chat_connect": "接続",
@@ -2358,10 +2446,12 @@
"ui.agent_chat_key_in_env": "キー設定済み",
"ui.agent_chat_left": "左",
"ui.agent_chat_limit": "エージェントが1メッセージあたりの変更上限 {limit} に達しました。続けさせるには、もう一度メッセージを送ってください。",
"ui.agent_chat_login_found": "ログイン情報あり",
"ui.agent_chat_minimize": "細いバーに最小化",
"ui.agent_chat_mode_all": "すべて",
"ui.agent_chat_mode_ask": "確認",
"ui.agent_chat_mode_edits": "編集",
"ui.agent_chat_model_no_tools": "{model} は SolidCraft のツールを使えず、チャットのみ可能です。ツール対応のモデルを選んでください。",
"ui.agent_chat_model_tip": "エージェントが使うモデル。選択はこのエージェントに対して記憶されます。",
"ui.agent_chat_new": "新しいチャット",
"ui.agent_chat_new_confirm": "新しいチャットを始めますか? 現在のチャットは消去されます。",
@@ -2370,6 +2460,7 @@
"ui.agent_chat_no_control": "エージェントチャットのエンドポイントが起動しなかったため、エージェントはデザインにアクセスできません。",
"ui.agent_chat_no_images": "このエージェントは画像を受け付けません",
"ui.agent_chat_no_picture": "クリップボードに画像がありません",
"ui.agent_chat_no_tool_call": "モデルが SolidCraft のツールを使わなかったため、デザインは変更されませんでした。ツール呼び出しに対応したモデルを選んでください。エージェントチャットのガイドの「ローカルモデル」を参照してください。",
"ui.agent_chat_none_found": "コーディングエージェントが見つかりません。次のいずれかをインストールしてから「再検索」を押してください:",
"ui.agent_chat_none_found_short": "エージェントが見つかりません",
"ui.agent_chat_not_installed": "未インストール",
@@ -2387,6 +2478,9 @@
"ui.agent_chat_previous_picture": "前の画像",
"ui.agent_chat_privacy": "あなたのアカウントで実行するエージェント以外には、何も送信されません。",
"ui.agent_chat_privacy_note": "SolidCraft はどこにも何も送信しません。あなたが実行するエージェントが、あなた自身のアカウントでそのサービスと通信します。SolidCraft がキーやパスワードを見たり保存したりすることはありません。",
"ui.agent_chat_provider_models": "{count} 個のモデル",
"ui.agent_chat_provider_other": "その他",
"ui.agent_chat_provider_tip": "隣の一覧にモデルを表示するプロバイダー。",
"ui.agent_chat_queue": "待機に追加",
"ui.agent_chat_queue_full": "待機中のメッセージが多すぎます。削除するか、エージェントの完了をお待ちください。",
"ui.agent_chat_queued": "待機中",
@@ -2455,6 +2549,7 @@
"ui.ai_http_busy": "エンドポイントが混雑しているか上限に達しています。後で再試行してください",
"ui.ai_http_key": "エンドポイントがキーを拒否しました。「環境設定 > AI」で確認してください",
"ui.ai_http_not_found": "エンドポイントまたはモデルが見つかりません。ベース URL とモデル名を確認してください",
"ui.ai_http_too_large": "会話がこのエンドポイントには大きすぎます。新しいチャットを始めてください",
"ui.ai_key": "API キー",
"ui.ai_key_hint": "任意。システムのキーチェーンに保存",
"ui.ai_key_needs_https": "キーは HTTPS 経由、またはこのコンピューター宛てにのみ送信されます。",
@@ -2501,9 +2596,27 @@
"ui.ai_test_no_model": "接続成功: {count} 個のモデル({model} はありません)",
"ui.ai_test_ok": "接続成功: {count} 個のモデル",
"ui.ai_testing": "テスト中…",
"ui.ai_tool_arguments": "引数",
"ui.ai_tool_details": "詳細",
"ui.ai_tool_done": "完了",
"ui.ai_tool_failed": "失敗",
"ui.ai_tool_result": "結果",
"ui.ai_tool_running": "実行中",
"ui.ai_tool_sum_bodies_one": "{count} ボディ",
"ui.ai_tool_sum_bodies_other": "{count} ボディ",
"ui.ai_tool_sum_by": "距離 {value}",
"ui.ai_tool_sum_edges_one": "{count} エッジ",
"ui.ai_tool_sum_edges_other": "{count} エッジ",
"ui.ai_tool_sum_faces_one": "{count} 面",
"ui.ai_tool_sum_faces_other": "{count} 面",
"ui.ai_tool_sum_features_one": "{count} フィーチャー",
"ui.ai_tool_sum_features_other": "{count} フィーチャー",
"ui.ai_tool_sum_measured": "測定: {what}",
"ui.ai_tool_sum_more": "{steps}、ほか {count} 件",
"ui.ai_tool_sum_radius": "R {value}",
"ui.ai_tool_sum_saved": "{file} を保存しました",
"ui.ai_tool_sum_screenshot": "スクリーンショット",
"ui.ai_tool_sum_then": "{first}、続いて {next}",
"ui.ai_tool_unfold": "クリックして結果を表示",
"ui.ai_tool_waiting": "待機中",
"ui.ai_toolbar_button": "アプリケーションバーにアシスタントボタンを表示",
@@ -2594,6 +2707,8 @@
"ui.copy": "コピー",
"ui.corner": "コーナー",
"ui.corners": "コーナー",
"ui.cosmetic_thread_markings": "ねじの簡略表示",
"ui.cosmetic_thread_markings_help": "ねじ付き円筒面に簡略化したマークを表示します。表示のみを変更し、設計やエクスポートには影響しません。",
"ui.counterbore": "座ぐり",
"ui.countersink": "皿穴",
"ui.create": "作成",
@@ -2670,6 +2785,8 @@
"ui.filter_commands_or_keys": "コマンドまたはキーを検索",
"ui.findInTimeline": "タイムラインで検索",
"ui.findInWindow": "ウィンドウで検索",
"ui.fine": "細かい",
"ui.finest": "最も細かい",
"ui.finish_sketch": "スケッチを終了",
"ui.fit": "フィット",
"ui.fix_unfix": "固定/固定解除",
@@ -2759,6 +2876,7 @@
"ui.heading_torus": "トーラス",
"ui.heading_utilities": "ユーティリティ",
"ui.heading_utility": "ユーティリティ",
"ui.heading_weldments": "溶接構造",
"ui.help": "ヘルプ",
"ui.hide": "非表示",
"ui.hideAxes": "軸を非表示",
@@ -2845,6 +2963,11 @@
"ui.move": "移動",
"ui.multiple": "複数",
"ui.named_views": "名前付きビュー",
"ui.nav_bridge": "このコンピューター上のデバイスアプリで操作",
"ui.nav_bridge_connected": "接続済み",
"ui.nav_bridge_port": "ポート",
"ui.nav_bridge_tip": "3D 入力デバイス専用のプログラム(自作 3D マウスのドライバーなど)がローカル接続でビューを動かすので、マウスポインターはそのまま使えます。SolidCraft はこのコンピューター上のこのポートにだけ接続します。",
"ui.nav_bridge_waiting": "デバイスアプリを待機中",
"ui.navigation": "ナビゲーション",
"ui.needs_design": "{command}: 先にデザインを開いてください",
"ui.newSketchGroup": "新しいスケッチグループ",
@@ -2863,6 +2986,7 @@
"ui.one_side": "片側",
"ui.open": "開く",
"ui.open_ends_tip": "どこにも接していない",
"ui.open_linked_design": "リンクされたデザインを開く",
"ui.open_preferences": "環境設定を開く",
"ui.open_recent": "最近使った項目を開く",
"ui.operation": "操作",
@@ -2882,18 +3006,27 @@
"ui.paste": "貼り付け",
"ui.path": "パス",
"ui.pattern_on_path": "パス上のパターン",
"ui.perforate_margin_default": "直径と同じ",
"ui.perforate_pitch": "ピッチ",
"ui.perforate_square": "正方配列",
"ui.perforate_staggered": "千鳥配列(60°)",
"ui.perforate_title": "パーフォレーション",
"ui.perpendicular": "直交",
"ui.perspective_view": "パースペクティブ表示",
"ui.pinToShortcuts": "ショートカットに固定",
"ui.pinToToolbar": "ツールバーに固定",
"ui.pipe": "パイプ",
"ui.placement": "配置",
"ui.placement_handles": "配置ハンドル",
"ui.placement_handles_tip": "レスト、ボス、スナップ フィットのプレビューに表示されるハンドル: マーカーをドラッグしてモデル上を移動し、リングで回転します",
"ui.plane": "平面",
"ui.plane_at_angle": "傾斜平面",
"ui.plugins": "プラグイン",
"ui.plugins_desktop": "プラグインはデスクトップ版で使えます。",
"ui.plugins_empty": "プラグインは入っていません。",
"ui.plugins_none": "検索に一致するプラグインはありません。",
"ui.plugins_note": "プラグインは {path} の中のフォルダで、plugin.json を置きます。SolidCraft はそのファイルを読むだけで、フォルダのコードは実行しません。オフにしてもインストールは残ります。",
"ui.plugins_search": "プラグインを検索",
"ui.plugins_skipped": "{count} 個のフォルダを読めませんでした。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2909,6 +3042,7 @@
"ui.project_face_edges_sketch": "新規スケッチに面のエッジを自動投影",
"ui.properties": "プロパティ",
"ui.pull_direction": "抜き方向",
"ui.quality": "品質",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "終了",
@@ -3015,6 +3149,7 @@
"ui.spline": "スプライン",
"ui.split_body": "ボディを分割",
"ui.stand_up_tip": "Z軸上向きで開いたSTEPまたはIGESファイルに対し、X軸周りに90度回転させるか確認します",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "最初からやり直す",
"ui.start_page": "スタートページ",
@@ -3054,6 +3189,7 @@
"ui.total_angle": "合計角度",
"ui.trackpad_pan_tip": "2本指スクロールでパンします。FusionではShift + 2本指スクロールで回転します。オフの場合はスクロールでズームします。マウスホイールとピンチ操作はズーム、サイドホイールは横方向のパンになります。",
"ui.trim": "トリム",
"ui.turn_up_axis": "上方向の軸を回転",
"ui.twist": "ねじれ",
"ui.two_finger_scroll_pans": "2本指スクロールでパン",
"ui.two_sides": "両側",
@@ -3071,6 +3207,7 @@
"ui.updateToCurrentView": "現在のビューに更新",
"ui.vertical_constraint": "垂直拘束",
"ui.view_centre": "ビュー中心",
"ui.viewport_theme_follow": "テーマに従う",
"ui.visual_shaded_hidden_edges": "隠線付きシェーディング",
"ui.visual_shaded_with_edges": "エッジ付きシェーディング",
"ui.visual_style": "表示スタイル",
@@ -3124,6 +3261,7 @@
"update.prefs_flatpak": "このコピー ({version}) は Flatpak が更新します: {command} を実行するか、ソフトウェアセンターを使ってください。SolidCraft 自身は確認しません。",
"update.prefs_label": "アップデート",
"update.prefs_note": "これは SolidCraft {version} ({kind}) です。確認では GitHub に最新リリースを問い合わせるだけで、ほかには何も送信しません。",
"update.prefs_web": "ページを再読み込みすると、最新のウェブアプリを取得します。ネットワークが利用できない場合や、更新のダウンロードが時間内に完了しない場合は、保存済みのバージョンを開きます。進行中のダウンロードは次回の再読み込みに向けて続行されます。",
"update.release_page": "リリースページを開く",
"update.restart": "今すぐ再起動",
"update.restart_later": "後で",
@@ -3134,6 +3272,12 @@
"update.up_to_date": "SolidCraft は最新です。",
"update.update_now": "今すぐ更新",
"update.you_have": "現在のバージョンは {version} です。",
"view.auto_ortho_faces": "回転中は透視投影、面を正面に見ると平行投影",
"web.files.insert_drawings_hint": "CADファイル、メッシュ、DXF、SVGを開いているデザインに挿入",
"web.files.insert_stored_drawings_hint": "開いているデザインに挿入(CADファイル、メッシュ、DXF、SVG)",
"web.open_full": "SolidCraft で開く",
"web.open_link": "リンク先のデザインを開きますか?",
"web.open_link_confirm": "保存していない変更があります。リンク先のデザインを新しいタブで開きますか?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 選択 · Enter / Tab 補完 · Esc 閉じる",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 選択 · Enter 入力どおりに適用 · Esc 閉じる"
}
+14 -1
View File
@@ -348,7 +348,7 @@
"Profile / Edges": "프로파일 / 모서리",
"Profiles or curves": "프로파일 또는 곡선",
"Profiles/Curves": "프로파일/곡선",
"Quantity": "양",
"Quantity": "수량",
"RECENT": "최근",
"REFERENCE": "참조",
"RULED SURFACE": "룰드 서피스",
@@ -1151,6 +1151,7 @@
"motion.key_here": "여기에 키프레임 추가",
"motion.key_here_hint": "이 단계에 조인트의 현재 값으로 키프레임 추가",
"motion.no_collisions": "연구 전체에서 충돌이 없습니다",
"motion.plot_quantity": "양",
"motion.step_back": "이전 단계",
"motion.step_forward": "다음 단계",
"motion.to_end": "끝으로",
@@ -1163,6 +1164,7 @@
"nav.fusion.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, 오른쪽 드래그는 회전입니다",
"nav.inventor.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, Ctrl+가운데 드래그는 확대/축소, 오른쪽 드래그는 회전입니다",
"nav.openscad.help": "빈 공간에서 왼쪽 드래그는 회전, Shift+왼쪽 드래그는 상자 선택, 스케치 형상 드래그는 편집입니다. 오른쪽 또는 가운데 드래그는 이동, Shift+오른쪽 또는 Shift+가운데 드래그는 확대/축소입니다",
"nav.perspective.sketch": "스케치 편집 중에는 직교 뷰를 사용합니다. 원근 뷰를 전환하려면 스케치를 마치세요.",
"nav.solidworks.help": "가운데 드래그는 회전, Ctrl+가운데 드래그는 이동, Shift+가운데 드래그는 확대/축소, 오른쪽 드래그는 회전입니다",
"nav.tinkercad.help": "오른쪽 드래그는 회전, 가운데 또는 Shift+오른쪽 드래그는 이동입니다",
"nav.touchpad.help": "Shift를 누른 채 포인터를 움직이면 화면이 이동하고, Alt/Option을 누른 채 움직이면 회전합니다. 마우스 버튼은 필요 없습니다. 세로 스크롤로 확대/축소합니다.",
@@ -1306,6 +1308,8 @@
"prefs.units_precision": "단위 및 정밀도",
"prefs.updates": "업데이트",
"prefs.view": "뷰 동작",
"prefs.view_confirm_corner": "뷰 모서리에 확인 및 취소",
"prefs.view_confirm_corner_note": "대화 상자나 스케치가 열려 있는 동안 뷰 오른쪽 위의 체크와 X 표시가 대화 상자의 확인과 취소와 같은 동작을 하거나 스케치를 마치고 취소합니다. 뷰 탐색기는 그 아래로 내려갑니다.",
"prefs.viewport": "뷰포트",
"prefs.wheel": "휠",
"prefs.wheel_trackpad": "휠 및 트랙패드",
@@ -1810,6 +1814,7 @@
"ui.ai_http_busy": "엔드포인트가 바쁘거나 한도에 도달했습니다. 나중에 다시 시도하세요",
"ui.ai_http_key": "엔드포인트가 키를 거부했습니다. 환경 설정 > AI에서 확인하세요",
"ui.ai_http_not_found": "엔드포인트나 모델을 찾을 수 없습니다. 기본 URL과 모델 이름을 확인하세요",
"ui.ai_http_too_large": "대화가 이 엔드포인트에 비해 너무 큽니다. 새 채팅을 시작하세요",
"ui.ai_key": "API 키",
"ui.ai_key_hint": "선택 사항, 시스템 키체인에 저장",
"ui.ai_key_needs_https": "키는 HTTPS를 통해서만, 또는 이 컴퓨터로만 전송됩니다.",
@@ -2013,6 +2018,8 @@
"ui.filter_commands_or_keys": "명령 또는 키 필터",
"ui.findInTimeline": "타임라인에서 찾기",
"ui.findInWindow": "창에서 찾기",
"ui.fine": "세밀",
"ui.finest": "가장 세밀",
"ui.finish_sketch": "스케치 종료",
"ui.fit": "맞춤",
"ui.fix_unfix": "고정/고정 해제",
@@ -2101,6 +2108,7 @@
"ui.heading_torus": "토러스",
"ui.heading_utilities": "유틸리티",
"ui.heading_utility": "유틸리티",
"ui.heading_weldments": "용접 구조물",
"ui.help": "도움말",
"ui.hide": "숨기기",
"ui.hideAxes": "축 숨기기",
@@ -2198,6 +2206,7 @@
"ui.one_side": "한 방향",
"ui.open": "열기",
"ui.open_ends_tip": "아무것과도 닿지 않음",
"ui.open_linked_design": "연결된 디자인 열기",
"ui.open_preferences": "환경 설정 열기",
"ui.open_recent": "최근 항목 열기",
"ui.operation": "작업",
@@ -2244,6 +2253,7 @@
"ui.project_face_edges_sketch": "면 모서리를 새 스케치로 투영",
"ui.properties": "속성",
"ui.pull_direction": "빼기 방향",
"ui.quality": "품질",
"ui.quantity": "수량",
"ui.quantity_2": "수량 2",
"ui.quit": "종료",
@@ -2344,6 +2354,7 @@
"ui.spline": "스플라인",
"ui.split_body": "바디 분할",
"ui.stand_up_tip": "STEP 또는 IGES 파일이 Z 위쪽으로 열린 후 X축 기준 90도 회전을 제안",
"ui.standard": "표준",
"ui.start": "시작",
"ui.start_over": "처음부터 다시",
"ui.start_page": "시작 페이지",
@@ -2397,6 +2408,7 @@
"ui.up_axis_note": "뷰(축 뷰, 홈, 보기)에서 위쪽을 가리키는 축입니다. 디자인은 변경되지 않습니다.",
"ui.updateToCurrentView": "현재 보기로 업데이트",
"ui.view_centre": "뷰 중심",
"ui.viewport_theme_follow": "테마 따르기",
"ui.visual_shaded_hidden_edges": "음영 처리 및 숨겨진 모서리",
"ui.visual_shaded_with_edges": "음영 처리 및 모서리",
"ui.visual_style": "시각 스타일",
@@ -2446,6 +2458,7 @@
"update.prefs_flatpak": "이 사본({version})은 Flatpak이 업데이트합니다: {command}를 실행하거나 소프트웨어 센터를 사용하세요. SolidCraft는 직접 확인하지 않습니다.",
"update.prefs_label": "업데이트",
"update.prefs_note": "SolidCraft {version} ({kind})입니다. 확인 시 GitHub에 최신 릴리스만 요청하며 다른 정보는 보내지 않습니다.",
"update.prefs_web": "페이지를 새로고침하면 최신 웹 앱을 가져옵니다. 네트워크를 사용할 수 없거나 업데이트 다운로드가 제한 시간 안에 완료되지 않으면 저장된 버전이 열립니다. 진행 중인 다운로드는 다음 새로고침을 위해 계속됩니다.",
"update.release_page": "릴리스 페이지 열기",
"update.restart": "지금 다시 시작",
"update.restart_later": "나중에",
+7
View File
@@ -971,6 +971,7 @@
"motion.key_here": "Kunci di sini",
"motion.key_here_hint": "Tambah kekunci pada langkah ini dengan nilai sendi sekarang",
"motion.no_collisions": "Tiada perlanggaran sepanjang kajian",
"motion.plot_quantity": "Kuantiti",
"motion.step_back": "Langkah ke belakang",
"motion.step_forward": "Langkah ke hadapan",
"motion.to_end": "Ke penghujung",
@@ -1389,6 +1390,7 @@
"ui.ai_http_busy": "titik akhir sibuk atau telah melebihi had; cuba lagi kemudian",
"ui.ai_http_key": "kunci ditolak oleh titik akhir: semak dalam Tetapan > AI",
"ui.ai_http_not_found": "titik akhir atau model tidak ditemui: semak URL asas dan nama model",
"ui.ai_http_too_large": "perbualan terlalu besar untuk titik akhir ini; mulakan sembang baharu",
"ui.ai_key": "Kunci API",
"ui.ai_key_hint": "tidak wajib; disimpan dalam rantai kunci sistem",
"ui.ai_key_needs_https": "Kunci hanya dihantar melalui HTTPS, atau ke komputer ini.",
@@ -1592,6 +1594,8 @@
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
"ui.findInTimeline": "Cari dalam Garis Masa",
"ui.findInWindow": "Cari dalam Tetingkap",
"ui.fine": "Halus",
"ui.finest": "Paling halus",
"ui.finish_sketch": "Tamatkan Lakaran",
"ui.fit": "Muatkan",
"ui.fix_unfix": "Tetap/Lepas",
@@ -1680,6 +1684,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "UTILITI",
"ui.heading_utility": "UTILITI",
"ui.heading_weldments": "KIMPALAN",
"ui.help": "Bantuan",
"ui.hide": "Sembunyi",
"ui.hideAxes": "Sembunyi Paksi",
@@ -1816,6 +1821,7 @@
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
"ui.properties": "Sifat",
"ui.pull_direction": "Arah tarikan",
"ui.quality": "Kualiti",
"ui.quantity": "Kuantiti",
"ui.quantity_2": "Kuantiti 2",
"ui.quit": "Keluar",
@@ -1910,6 +1916,7 @@
"ui.spline": "Splin",
"ui.split_body": "Belah Jasad",
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
"ui.standard": "Standard",
"ui.start": "Mula",
"ui.start_over": "Mula semula",
"ui.start_page": "Halaman mula",
File diff suppressed because it is too large Load Diff
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "Sleutel hier",
"motion.key_here_hint": "Voeg een sleutel toe op deze stap met de huidige waarde van de verbinding",
"motion.no_collisions": "Geen botsingen in de studie",
"motion.plot_quantity": "Aantal",
"motion.step_back": "Stap terug",
"motion.step_forward": "Stap verder",
"motion.to_end": "Naar het einde",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "het eindpunt is bezet of boven zijn limiet; probeer het later opnieuw",
"ui.ai_http_key": "het eindpunt weigerde de sleutel: controleer deze in Voorkeuren > AI",
"ui.ai_http_not_found": "geen eindpunt of model gevonden: controleer de basis-URL en de modelnaam",
"ui.ai_http_too_large": "het gesprek is te groot voor dit endpoint; begin een nieuwe chat",
"ui.ai_key": "API-sleutel",
"ui.ai_key_hint": "optioneel; bewaard in de sleutelhanger van het systeem",
"ui.ai_key_needs_https": "Een sleutel wordt alleen via HTTPS of naar deze computer verzonden.",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
"ui.findInTimeline": "Zoeken in tijdlijn",
"ui.findInWindow": "Zoeken in venster",
"ui.fine": "Fijn",
"ui.finest": "Zeer fijn",
"ui.finish_sketch": "Schets voltooien",
"ui.fit": "Passend",
"ui.fix_unfix": "Vastzetten/losmaken",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "HULPPROGRAMMA'S",
"ui.heading_utility": "HULPPROGRAMMA",
"ui.heading_weldments": "LASWERK",
"ui.help": "Help",
"ui.hide": "Verbergen",
"ui.hideAxes": "Assen verbergen",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
"ui.properties": "Eigenschappen",
"ui.pull_direction": "Trekrichting",
"ui.quality": "Kwaliteit",
"ui.quantity": "Aantal",
"ui.quantity_2": "Aantal 2",
"ui.quit": "Afsluiten",
@@ -1852,6 +1858,7 @@
"ui.spline": "Spline",
"ui.split_body": "Lichaam splitsen",
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
"ui.standard": "Standaard",
"ui.start": "Start",
"ui.start_over": "Opnieuw beginnen",
"ui.start_page": "Startpagina",
+21
View File
@@ -124,6 +124,11 @@
"Don't ask again": "Nie pytaj ponownie",
"Double-click to rename": "Kliknij dwukrotnie, aby zmienić nazwę",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Dwukrotne kliknięcie środkowym przyciskiem w widoku: Widok główny (kadruje to, co widoczne), Dopasuj (kadruje to, co widoczne, zachowując orientację) lub nic",
"Drag to move the boss across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć występ po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć podparcie po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć zatrzask po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to point the catch in another direction.": "Przeciągnij, aby skierować zaczep w inną stronę.",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
"Drive Joint": "Steruj złączem",
"Duration": "Czas trwania",
"Edges": "Krawędzie",
@@ -568,6 +573,7 @@
"cmd.document_metadata": "Właściwości projektu",
"cmd.document_metadata_remove": "Usuń właściwość projektu",
"cmd.document_metadata_set": "Zmień właściwości projektu",
"cmd.document_revision": "Wersja projektu",
"cmd.document_units": "Zmień jednostki projektu",
"cmd.edit_checkpoint": "Punkt kontrolny",
"cmd.edit_restore_checkpoint": "Przywróć punkt kontrolny",
@@ -1026,6 +1032,7 @@
"motion.key_here": "Wstaw klatkę kluczową tutaj",
"motion.key_here_hint": "Dodaj klatkę kluczową w tym kroku z bieżącą wartością złącza",
"motion.no_collisions": "Brak kolizji w trakcie badania",
"motion.plot_quantity": "Liczba",
"motion.step_back": "Krok wstecz",
"motion.step_forward": "Krok naprzód",
"motion.to_end": "Na koniec",
@@ -1131,6 +1138,7 @@
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
"prefs.orientation": "Orientacja",
"prefs.overall_speed": "Szybkość ogólna",
"prefs.placement": "Umieszczanie",
"prefs.preview_choices": "Podgląd wybranych opcji",
"prefs.quality_default": "domyślnie",
"prefs.quality_fastest": "najszybciej",
@@ -1575,6 +1583,7 @@
"ui.ai_http_busy": "punkt końcowy jest zajęty lub przekroczył limit; spróbuj ponownie później",
"ui.ai_http_key": "punkt końcowy odrzucił klucz: sprawdź go w Preferencje > AI",
"ui.ai_http_not_found": "nie ma takiego punktu końcowego lub modelu: sprawdź bazowy URL i nazwę modelu",
"ui.ai_http_too_large": "rozmowa jest za duża dla tego punktu końcowego; rozpocznij nowy czat",
"ui.ai_key": "Klucz API",
"ui.ai_key_hint": "opcjonalny; przechowywany w systemowym pęku kluczy",
"ui.ai_key_needs_https": "Klucz jest wysyłany tylko przez HTTPS albo do tego komputera.",
@@ -1778,6 +1787,8 @@
"ui.filter_commands_or_keys": "Filtruj polecenia lub klawisze",
"ui.findInTimeline": "Znajdź na osi czasu",
"ui.findInWindow": "Znajdź w oknie",
"ui.fine": "Dokładna",
"ui.finest": "Najdokładniejsza",
"ui.finish_sketch": "Zakończ szkic",
"ui.fit": "Dopasuj",
"ui.fix_unfix": "Ustal/zwolnij",
@@ -1866,6 +1877,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "NARZĘDZIA",
"ui.heading_utility": "NARZĘDZIE",
"ui.heading_weldments": "SPAWANIE",
"ui.help": "Pomoc",
"ui.hide": "Ukryj",
"ui.hideAxes": "Ukryj osie",
@@ -1945,6 +1957,11 @@
"ui.move": "Przesuń",
"ui.multiple": "Wiele",
"ui.named_views": "Widoki nazwane",
"ui.nav_bridge": "Nawiguj programem urządzenia na tym komputerze",
"ui.nav_bridge_connected": "Połączono",
"ui.nav_bridge_port": "Port",
"ui.nav_bridge_tip": "Własny program urządzenia wejściowego 3D (np. sterownik domowej myszy 3D) porusza widokiem przez połączenie lokalne, więc kursor myszy zostaje Twój. SolidCraft łączy się z nim na tym porcie, tylko na tym komputerze.",
"ui.nav_bridge_waiting": "Czekam na program urządzenia",
"ui.navigation": "Nawigacja",
"ui.needs_design": "{command}: najpierw otwórz projekt",
"ui.newSketchGroup": "Nowa grupa szkiców",
@@ -1988,6 +2005,8 @@
"ui.pinToToolbar": "Przypnij do paska narzędzi",
"ui.pipe": "Rura",
"ui.placement": "Umiejscowienie",
"ui.placement_handles": "Uchwyty umieszczania",
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
"ui.plane": "Płaszczyzna",
"ui.plane_at_angle": "Płaszczyzna pod kątem",
"ui.plugins": "Wtyczki",
@@ -2009,6 +2028,7 @@
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
"ui.properties": "Właściwości",
"ui.pull_direction": "Kierunek wyjmowania",
"ui.quality": "Jakość",
"ui.quantity": "Liczba",
"ui.quantity_2": "Liczba 2",
"ui.quit": "Zakończ",
@@ -2103,6 +2123,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Podziel bryłę",
"ui.stand_up_tip": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X",
"ui.standard": "Standardowa",
"ui.start": "Start",
"ui.start_over": "Zacznij od nowa",
"ui.start_page": "Strona startowa",
+334
View File
@@ -14,11 +14,15 @@
"Active": "Ativo",
"Active rule": "Regra ativa",
"Add": "Adicionar",
"Add Column": "Adicionar coluna",
"Add Key": "Adicionar chave",
"Add Mid Radius": "Adicionar raio intermediário",
"Add Row": "Adicionar linha",
"Add a tool": "Adicionar ferramenta",
"Add condition": "Adicionar condição",
"Add or edit rule": "Adicionar ou editar regra",
"Add to New Folder": "Adicionar à nova pasta",
"Advanced": "Avançado",
"Against": "Contra",
"Alignment": "Alinhamento",
"All Sketch Points": "Todos os pontos do esboço",
@@ -28,9 +32,11 @@
"Also deletes": "Também exclui",
"Angle 2": "Ângulo 2",
"Animate": "Animar",
"App/system information and command settings are included. Review private details before sharing.": "Informações do aplicativo/sistema e configurações estão incluídas. Revise detalhes privados antes de compartilhar.",
"Appearance name": "Nome da aparência",
"Applied Radius": "Raio aplicado",
"Apply To": "Aplicar a",
"Apply library appearance": "Aplicar aparência da biblioteca",
"Apply to selection": "Aplicar à seleção",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Aplique a aparência primeiro. Exporta as aparências atribuídas e os materiais criados neste projeto.",
"Arc length": "Comprimento do arco",
@@ -41,13 +47,18 @@
"BLEND SURFACE": "SUPERFÍCIE DE TRANSIÇÃO",
"BOUNDARY SURFACE": "SUPERFÍCIE DE CONTORNO",
"BROWSER": "NAVEGADOR",
"Bend Edges": "Arestas de dobra",
"Bend Line Position": "Posição da linha de dobra",
"Bend Position": "Posição da dobra",
"Bend Radius": "Raio de dobra",
"Bend Table": "Tabela de dobras",
"Bends": "Dobras",
"Bodies": "Corpos",
"Bodies (optional)": "Corpos (opcional)",
"Body": "Corpo",
"Body (last if none)": "Corpo (o último, se nenhum)",
"Body to split": "Corpo a dividir",
"Body, or faces to take out": "Corpo, ou faces a remover",
"Boundary": "Contorno",
"Bounding box": "Caixa delimitadora",
"CHECK": "VERIFICAR",
@@ -59,7 +70,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV ou JSON: atualiza parâmetros existentes e adiciona novos",
"Capture All Positions": "Capturar todas as posições",
"Capture Pos.": "Capturar pos.",
"Catch Depth": "Profundidade do encaixe",
"Catch Direction": "Direção do encaixe",
"Catch Length": "Comprimento do encaixe",
"Cells": "Células",
"Center X": "Centro X",
"Center Y": "Centro Y",
@@ -68,23 +81,31 @@
"Chamfer Type": "Tipo de chanfro",
"Check collisions": "Verificar colisões",
"Checked": "Marcado",
"Choose Variable radius or Spline in Type.": "Escolha Raio variável ou Spline em Tipo.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Escolha um novo plano. A geometria e as cotas do esboço permanecem inalteradas em suas coordenadas bidimensionais.",
"Choose…": "Escolher…",
"Circular": "Circular",
"Circumference": "Circunferência",
"Clockwise": "Horário",
"Close draft": "Fechar rascunho",
"Collapse": "Recolher",
"Collisions": "Colisões",
"Colour": "Cor",
"Command": "Comando",
"Comment": "Comentário",
"Component 1": "Componente 1",
"Component 2": "Componente 2",
"Component1": "Componente1",
"Components": "Componentes",
"Config Table": "Tabela de config.",
"Configuration": "Configuração",
"Configuration rules": "Regras de configuração",
"Contact": "Contato",
"Contact Sets": "Conjuntos de contato",
"Contact sets": "Conjuntos de contato",
"Continue on GitHub": "Continuar no GitHub",
"Continuity": "Continuidade",
"Control Radius": "Raio de controle",
"Copy Error Message": "Copiar mensagem de erro",
"Copy Warning Message": "Copiar mensagem de aviso",
"Copy all licences": "Copiar todas as licenças",
@@ -94,8 +115,11 @@
"Corner Relief": "Alívio de canto",
"Corner Relief Shape": "Forma do alívio de canto",
"Counterbore depth": "Profundidade do rebaixo",
"Counterbore Ø": "Ø do rebaixo",
"Countersink angle": "Ângulo do escareado",
"Countersink Ø": "Ø do escareado",
"Create Issue — review draft": "Criar issue — revisar rascunho",
"Create Issue…": "Criar issue…",
"Cross curves": "Curvas transversais",
"Cube": "Cubo",
"Current orthographic view (1:1 mm)": "Vista ortográfica atual (1:1 mm)",
@@ -103,18 +127,28 @@
"Curv. Comb": "Pente de curv.",
"Curvature (G2)": "Curvatura (G2)",
"Curvature change (G3)": "Variação de curvatura (G3)",
"Custom": "Personalizado",
"Customize Toolbar": "Personalizar barra de ferramentas",
"Customize…": "Personalizar…",
"Cylindrical face": "Face cilíndrica",
"Delete": "Excluir",
"Delete Selected Features…": "Excluir recursos selecionados…",
"Delete Selected Folders": "Excluir pastas selecionadas",
"Delete all": "Excluir tudo",
"Delete rule": "Excluir regra",
"Delete the row": "Excluir a linha",
"Delete the set": "Excluir o conjunto",
"Density": "Densidade",
"Dependents": "Dependentes",
"Describe the problem you saw": "Descreva o problema que você viu",
"Describe what happened before continuing.": "Descreva o que aconteceu antes de continuar.",
"Designs you open or save appear here.": "Os projetos que você abrir ou salvar aparecem aqui.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por Até o objeto. Escolha Ângulo para editar a extensão angular.",
"Diameter": "Diâmetro",
"Direction": "Direção",
"Discard": "Descartar",
"Distance": "Distância",
"Distance 1": "Distância 1",
"Do not export hidden edges": "Não exportar arestas ocultas",
"Don't ask again": "Não perguntar novamente",
"Double-click to rename": "Clique duas vezes para renomear",
@@ -126,49 +160,65 @@
"Edit Profile Sketch": "Editar esboço do perfil",
"Enable rule": "Ativar regra",
"Enabled": "Ativado",
"End Radius": "Raio final",
"End bulge": "Abaulamento final",
"End continuity": "Continuidade final",
"Expand": "Expandir",
"Expected result (optional)": "Resultado esperado (opcional)",
"Explode": "Explodir",
"Explode Text": "Decompor texto",
"Export Animation…": "Exportar animação…",
"Export Bend Table…": "Exportar tabela de dobras…",
"Export CSV…": "Exportar CSV…",
"Export Current View as DXF": "Exportar vista atual como DXF",
"Export Current View as DXF…": "Exportar vista atual como DXF…",
"Export DXF": "Exportar DXF",
"Export DXF…": "Exportar DXF…",
"Export JSON…": "Exportar JSON…",
"Export design library...": "Exportar biblioteca do projeto...",
"Export hidden edges as dashed lines": "Exportar arestas ocultas como linhas tracejadas",
"Exported by SolidWorks, which usually saves Y up": "Exportado pelo SolidWorks, que normalmente salva com Y para cima",
"Expression": "Expressão",
"Extend to meet": "Estender até encontrar",
"Extents": "Extensões",
"FILES": "ARQUIVOS",
"FLAT": "PLANIFICAÇÃO",
"Face": "Face",
"Face Appearance": "Aparência da face",
"Face attachment lost": "Fixação à face perdida",
"Faces": "Faces",
"Faces / edges / vertices": "Faces / arestas / vértices",
"Faces to split": "Faces a dividir",
"Fastener": "Fixador",
"Favorites": "Favoritos",
"Favourite": "Favorito",
"Feature": "Recurso",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Arquivos de muitos programas CAD usam Y para cima, então abrem deitados aqui (Z para cima).",
"Filter": "Filtro",
"Filter…": "Filtrar…",
"Find in Browser": "Localizar no navegador",
"Finish": "Acabamento",
"First plane": "Primeiro plano",
"Fit and G2 corners require Constant radius.": "Ajuste e cantos G2 exigem Raio constante.",
"Fixed Face": "Face fixa",
"Flat Pattern": "Planificação",
"Flat size": "Tamanho planificado",
"Follow camera (Look At sketches)": "Seguir a câmera (Olhar para os esboços)",
"Follow the camera (Look At sketches)": "Seguir a câmera (Olhar para os esboços)",
"Font": "Fonte",
"From": "De",
"From Bodies": "A partir de corpos",
"From edges": "Arestas de origem",
"Full Length": "Comprimento total",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "Cantos G2 exigem raio constante e Ajuste exato. A construção de cantos G2 ainda não é suportada.",
"Gap": "Folga",
"Grid": "Grade",
"HISTORY": "HISTÓRICO",
"Height": "Altura",
"Hidden edges": "Arestas ocultas",
"History layout": "Layout do histórico",
"History names": "Nomes no histórico",
"Hole (optional)": "Furo (opcional)",
"Holes": "Furos",
"Holes (edges on them)": "Furos (arestas neles)",
"Home view": "Vista inicial",
@@ -179,14 +229,20 @@
"In place (surfaces)": "No lugar (superfícies)",
"In plane": "No plano",
"Inside": "Dentro",
"Instant": "Instantâneo",
"Interface font": "Fonte da interface",
"Interferes": "Interfere",
"Invert": "Inverter",
"Isocurve": "Isocurva",
"Issue title": "Título da issue",
"Items": "Itens",
"JOINTS": "JUNTAS",
"Joint Origin1": "Origem da junta1",
"Keep (points on it)": "Manter (pontos sobre ela)",
"Keep Tools": "Manter ferramentas",
"Keep of the other surface": "Manter da outra superfície",
"Key a joint's value at this step": "Criar um quadro-chave com o valor de uma junta neste passo",
"Keys": "Quadros-chave",
"LIBRARY": "BIBLIOTECA",
"LINKS": "VÍNCULOS",
"Later": "Depois",
@@ -197,9 +253,14 @@
"Letter Spacing": "Espaçamento entre letras",
"Library": "Biblioteca",
"Library file": "Arquivo de biblioteca",
"Limits": "Limites",
"Line Spacing": "Espaçamento entre linhas",
"Line Type": "Tipo de linha",
"Listing the installed fonts…": "Listando as fontes instaladas…",
"Load library": "Carregar biblioteca",
"Local library": "Biblioteca local",
"Loop": "Laço",
"M5 or M5x0.5": "M5 ou M5x0.5",
"MANAGE": "GERENCIAR",
"MATERIALS": "MATERIAIS",
"MODEL": "MODELO",
@@ -210,14 +271,20 @@
"Matched restrictions check the final values.": "As restrições cujas condições são atendidas verificam os valores finais.",
"Material": "Material",
"Max": "Máx.",
"Maximum": "Máximo",
"Mesh body": "Corpo de malha",
"Mid Radius": "Raio intermediário",
"Middle double-click": "Clique duplo do meio",
"Min": "Mín.",
"Minimum": "Mínimo",
"Mirror Plane": "Plano de espelho",
"Mitre": "Meia-esquadria",
"Model Parameters": "Parâmetros do modelo",
"Modeled": "Modelado",
"More toolbar panels": "Mais painéis da barra de ferramentas",
"Motion": "Movimento",
"Mouse gestures": "Gestos do mouse",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Os gestos do mouse só funcionam no modo de mouse SolidWorks (a roda orbita, Ctrl + roda desloca).",
"Move / Copy": "Mover / Copiar",
"Move Type": "Tipo de movimento",
"Move down": "Mover para baixo",
@@ -235,15 +302,22 @@
"No matching appearance presets.": "Nenhuma predefinição de aparência correspondente.",
"No matching appearances": "Nenhuma aparência correspondente",
"No visible bodies or sketches to export.": "Nenhum corpo ou esboço visível para exportar.",
"Nominal diameter": "Diâmetro nominal",
"None": "Nenhum",
"Not available yet": "Ainda não disponível",
"Notch": "Entalhe",
"OUTPUT": "SAÍDA",
"Of": "De",
"On your computer": "No seu computador",
"Opacity": "Opacidade",
"Open GitHub draft": "Abrir rascunho no GitHub",
"Open library...": "Abrir biblioteca...",
"Open local libraries in the desktop app. You can export this design's library here.": "Abra bibliotecas locais no aplicativo para desktop. Você pode exportar a biblioteca deste projeto aqui.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "CAD 3D paramétrico de código aberto em Rust. Licenciado sob MIT OU Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Abre o navegador e copia o texto do relatório para você colar no GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Abre o navegador. Revise e envie seu relatório no GitHub.",
"Operator": "Operador",
"Orbit (right drag)": "Orbitar (arrastar com o botão direito)",
"Other side": "Outro lado",
"Other surface (mutual trim)": "Outra superfície (aparo mútuo)",
"Outside": "Fora",
@@ -258,8 +332,11 @@
"Pick the tool bodies": "Selecione os corpos de ferramenta",
"Picked": "Selecionado",
"Picked point": "Ponto selecionado",
"Pitch": "Passo",
"Pivot": "Pivô",
"Place at the origin instead": "Colocar na origem",
"Plastic Rules": "Regras de plástico",
"Play the joint's motion in the view": "Reproduzir o movimento da junta na vista",
"Playback": "Reprodução",
"Plot": "Gráfico",
"Position": "Posição",
@@ -267,14 +344,18 @@
"Position (G0)": "Posição (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Posição ao longo da aresta: 0 na extremidade inicial, 1 na outra",
"Preferences › Design turns the offer back on": "Preferências › Projeto reativa a oferta",
"Preview": "Visualização",
"Preview Row": "Visualizar linha",
"Preview leaves the current design unchanged.": "A visualização não altera o projeto atual.",
"Profile / Edges": "Perfil / Arestas",
"Profiles or curves": "Perfis ou curvas",
"Profiles/Curves": "Perfis/Curvas",
"Quantity": "Quantidade",
"RECENT": "RECENTES",
"REFERENCE": "REFERÊNCIA",
"RULED SURFACE": "SUPERFÍCIE REGRADA",
"RULES": "REGRAS",
"Radius": "Raio",
"Radius Fit": "Ajuste de raio",
"Ratio": "Razão",
"Reattach selected face": "Fixar novamente na face selecionada",
@@ -283,6 +364,8 @@
"Rectangular": "Retangular",
"Reference face": "Face de referência",
"Remove Tools": "Remover ferramentas",
"Remove condition": "Remover condição",
"Remove from Folder": "Remover da pasta",
"Remove from list": "Remover da lista",
"Remove override": "Remover substituição",
"Remove private design details or paths before sharing.": "Remova detalhes privados do projeto ou caminhos antes de compartilhar.",
@@ -290,12 +373,18 @@
"Remove the column": "Remover a coluna",
"Remove the track": "Remover a trilha",
"Remove this point": "Remover este ponto",
"Rename Feature": "Renomear recurso",
"Rename Folder": "Renomear pasta",
"Replace existing library file": "Substituir o arquivo de biblioteca existente",
"Report a bug — review a screenshot and open a GitHub draft": "Relatar um bug — revise uma captura de tela e abra um rascunho no GitHub",
"Report text copied. Paste it into GitHub and submit your report there.": "Texto do relatório copiado. Cole-o no GitHub e envie seu relatório por lá.",
"Reports are public. A GitHub account is required.": "Os relatórios são públicos. É necessária uma conta no GitHub.",
"Reset Toolbar": "Redefinir barra de ferramentas",
"Resize Part": "Redimensionar peça",
"Restore View": "Restaurar vista",
"Restore outer boundary": "Restaurar contorno externo",
"Restore the inherited component or physical material appearance": "Restaurar a aparência herdada do componente ou do material físico",
"Restore this studio's tools": "Restaurar as ferramentas deste ambiente",
"Result": "Resultado",
"Retake screenshot": "Refazer captura de tela",
"Reverse": "Inverter",
@@ -305,7 +394,14 @@
"Revert Pos.": "Reverter pos.",
"Revert Position": "Reverter posição",
"Review the report and screenshot before opening GitHub.": "Revise o relatório e a captura de tela antes de abrir o GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Revise seu relatório no GitHub e clique em Submit new issue para publicá-lo.",
"Rib Length": "Comprimento da nervura",
"Rib Offset": "Deslocamento da nervura",
"Rib Thickness": "Espessura da nervura",
"Ribs": "Nervuras",
"Rim Edge": "Aresta da borda",
"Ring of tools on the right button": "Anel de ferramentas no botão direito",
"Roll Back Here": "Retroceder até aqui",
"Roll History Marker Here": "Mover o marcador do histórico para cá",
"Round": "Arredondado",
"Rule": "Regra",
@@ -315,21 +411,28 @@
"SURFACE QUALITY": "QUALIDADE DA SUPERFÍCIE",
"Save Rule": "Salvar regra",
"Save View": "Salvar vista",
"Save screenshot…": "Salvar captura de tela…",
"Save the study as an animated GIF, seen as the viewport shows it": "Salvar o estudo como GIF animado, como a janela de visualização o mostra",
"Saved preview. Geometry is being checked before editing.": "Visualização salva. A geometria está sendo verificada antes da edição.",
"Scale factor": "Fator de escala",
"Scene": "Cena",
"Screenshot (optional)": "Captura de tela (opcional)",
"Search": "Pesquisar",
"Search commands": "Pesquisar comandos",
"Search this library": "Pesquisar nesta biblioteca",
"Second plane": "Segundo plano",
"Section": "Seção",
"Section position": "Posição da seção",
"Sections (one sketch each)": "Seções (um esboço cada)",
"Select Other": "Selecionar outro",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Selecionar outro: mantenha o botão esquerdo parado sobre o modelo (ou clique com o botão direito › Selecionar outro…) para listar tudo sob o cursor; clique de novo no mesmo ponto para selecionar o próximo item ali. Clique duas vezes com o botão do meio para ir ao Início (Preferências › Navegação).",
"Select bodies (a face picks its body)": "Selecione corpos (uma face seleciona seu corpo)",
"Select bodies or faces instead": "Selecionar corpos ou faces em vez disso",
"Select edges": "Selecione arestas",
"Selection filter": "Filtro de seleção",
"Semantic faces share their appearance across instances.": "Faces semânticas compartilham a aparência entre instâncias.",
"Send your report on GitHub": "Envie seu relatório no GitHub",
"Shaded": "Sombreado",
"Shape": "Forma",
"Sheet Rules": "Regras de chapa",
"Show Agent Cursor": "Mostrar cursor do agente",
@@ -348,6 +451,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "O SolidCraft fechou antes de estes projetos serem salvos. O último salvamento automático deles pode ser aberto.",
"Specialise": "Especializar",
"Speed": "Velocidade",
"Splitting tool": "Ferramenta de divisão",
"Square": "Quadrado",
"Stand it up": "Colocar em pé",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Colocar em pé: um quarto de volta em torno de X, como um recurso Mover que pode ser desfeito.",
@@ -357,17 +461,27 @@
"Start continuity": "Continuidade inicial",
"Step": "Passo",
"Steps": "Passos",
"Steps to reproduce (optional)": "Passos para reproduzir (opcional)",
"Stitch the kept pieces": "Costurar as partes mantidas",
"Stitch to both bodies": "Costurar aos dois corpos",
"Studio": "Ambiente",
"Study": "Estudo",
"Style": "Estilo",
"Summary": "Resumo",
"Suppress": "Suprimir",
"Suppressed": "Suprimido",
"Surface": "Superfície",
"Surface, or faces to turn over": "Superfície, ou faces a inverter",
"Swap Ends": "Trocar extremidades",
"TRIM": "APARAR",
"Tangent (G1)": "Tangente (G1)",
"Tangent Chain": "Cadeia tangente",
"Tangent chain": "Cadeia tangente",
"Tap drill diameter": "Diâmetro da broca para macho",
"Target": "Alvo",
"Target Body": "Corpo-alvo",
"Tear": "Rasgo",
"Technical details": "Detalhes técnicos",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "O esboço tracejado é uma guia de plano salva. Selecione uma nova face para reparar a fixação.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "A ocorrência de autoria salva não existe mais. Selecione um posicionamento de autoria válido antes de vincular novamente.",
"Then": "Então",
@@ -379,24 +493,32 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Este filho é compartilhado. Excluí-lo o remove de todas as cópias do pai.",
"This child is shared. Position changes apply to every copy of its parent.": "Este filho é compartilhado. Mudanças de posição se aplicam a todas as cópias do pai.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta face não pode fornecer uma fixação duradoura. Escolha outra face ou use explicitamente um plano fixo.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Esta imagem ainda não está anexada no GitHub. Copie ou salve-a e depois anexe-a no GitHub.",
"To": "Para",
"To Offset": "Deslocamento final",
"To edges": "Arestas de destino",
"Tolerance": "Tolerância",
"Too close": "Muito próximo",
"Tool Bodies": "Corpos-ferramenta",
"Tools": "Ferramentas",
"Tools, clockwise from the top": "Ferramentas, em sentido horário a partir do topo",
"Trimming tool (plane, face or curves)": "Ferramenta de aparo (plano, face ou curvas)",
"Turn off section analysis before exporting the view.": "Desative a análise de seção antes de exportar a vista.",
"Type": "Tipo",
"UNTRIM": "DESAPARAR",
"Unit": "Unidade",
"Unsuppress": "Reativar",
"Updating the preview": "Atualizando a visualização",
"Use active author placement": "Usar o posicionamento de autoria ativo",
"Use fixed plane": "Usar plano fixo",
"Use saved fixed plane": "Usar o plano fixo salvo",
"Use selected face as fixed plane": "Usar a face selecionada como plano fixo",
"Use the SolidWorks mouse mode": "Usar o modo de mouse SolidWorks",
"User Parameters": "Parâmetros do usuário",
"VIEW": "VISTA",
"VIEWS": "VISTAS",
"Value": "Valor",
"Variable Radius": "Raio variável",
"Velocity": "Velocidade",
"Vertices": "Vértices",
"View navigator": "Navegador de vistas",
@@ -404,16 +526,50 @@
"Wall": "Parede",
"Wall Location": "Posição da parede",
"Wall Thickness": "Espessura da parede",
"What did you do just before the problem? A short list is enough": "O que você fez logo antes do problema? Uma lista curta basta",
"What did you expect to see?": "O que você esperava ver?",
"What happened?": "O que aconteceu?",
"When": "Quando",
"Width": "Largura",
"Will fail": "Vai falhar",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Com os gestos ativados, o botão direito não orbita mais a vista: segure-o e mova para abrir o anel, depois toque na zona de uma ferramenta. Um clique direito simples ainda abre o menu.",
"X": "X",
"X Angle": "Ângulo X",
"X angle": "Ângulo X",
"X distance": "Distância X",
"Y": "Y",
"Y Angle": "Ângulo Y",
"Y angle": "Ângulo Y",
"Y distance": "Distância Y",
"Z": "Z",
"Z Angle": "Ângulo Z",
"Z Distance": "Distância Z",
"Z angle": "Ângulo Z",
"Z distance": "Distância Z",
"Zebra": "Zebra",
"ai.prefs.availability": "Controles do Assistente",
"ai.prefs.background_commands": "Executar comandos do agente em segundo plano",
"ai.prefs.follow_camera": "Virar a câmera para os esboços",
"ai.prefs.follow_camera_off": "O acompanhamento automático da câmera está desativado.",
"ai.prefs.follow_camera_on": "A câmera se vira para os esboços que o agente cria.",
"ai.prefs.intro": "Agentes, permissões e como o trabalho deles aparece.",
"ai.prefs.make_default": "Usar como padrão",
"ai.prefs.no_agents": "Nenhum agente ainda. Adicione um agente ou atualize para encontrar agentes instalados.",
"ai.prefs.no_saved_approvals": "Nenhuma aprovação salva.",
"ai.prefs.override_all_warning": "Este agente pode salvar, sobrescrever, exportar ou substituir o projeto sem perguntar.",
"ai.prefs.permission_all": "Permitir tudo",
"ai.prefs.permission_all_summary": "As ferramentas disponíveis do SolidCraft são executadas sem perguntar.",
"ai.prefs.permission_ask": "Perguntar antes de alterar",
"ai.prefs.permission_ask_summary": "Aprovar cada alteração no projeto e cada ação de arquivo.",
"ai.prefs.permission_edits": "Permitir edições no modelo",
"ai.prefs.permission_edits_summary": "Edições no modelo são executadas; ações de arquivo e destrutivas ainda perguntam.",
"ai.prefs.permission_scope": "Só para ferramentas do SolidCraft. O shell e as edições de arquivos do agente usam as permissões dele.",
"ai.prefs.pointer_preview_hidden": "Ponteiro oculto",
"ai.prefs.pointer_preview_speed": "Ponteiro do agente · {speed}",
"ai.prefs.pointer_speed": "Velocidade do movimento",
"ai.prefs.privacy_note": "Só o agente ou endpoint que você escolher recebe a conversa. As chaves de endpoint ficam guardadas no chaveiro do sistema.",
"ai.prefs.show_pointer": "Mostrar ponteiro do agente",
"ai.prefs.turn_limit_hint": "Número máximo de chamadas de ferramenta que alteram o projeto que o Assistente pode fazer por mensagem.",
"angle, height": "ângulo, altura",
"appearance.dielectric_ior": "IOR dielétrico",
"appearance.metallic": "Metálico",
@@ -460,8 +616,10 @@
"cmd.component_activate": "Ativar componente",
"cmd.component_break_link": "Quebrar vínculo",
"cmd.component_capture_position": "Capturar posição",
"cmd.component_copies": "Encontrar cópias idênticas",
"cmd.component_create": "Novo componente",
"cmd.component_delete": "Excluir componente",
"cmd.component_extract": "Extrair para arquivo",
"cmd.component_from_bodies": "Criar componentes a partir de corpos",
"cmd.component_insert_design": "Inserir projeto",
"cmd.component_links": "Listar vínculos",
@@ -474,6 +632,7 @@
"cmd.component_properties": "Propriedades físicas do componente",
"cmd.component_relink": "Revincular origem",
"cmd.component_rename": "Renomear componente",
"cmd.component_replace_with_link": "Substituir por vínculo",
"cmd.component_revert_position": "Reverter posição",
"cmd.component_unlock": "Desbloquear conteúdo",
"cmd.component_update_links": "Atualizar vínculos",
@@ -596,6 +755,7 @@
"cmd.motion_plot": "Plotar estudo de movimento",
"cmd.motion_study": "Estudo de movimento",
"cmd.motion_trace": "Traçado de movimento",
"cmd.motion_trace_edit": "Editar traçado de movimento",
"cmd.motion_trace_keep": "Manter traçado de movimento",
"cmd.occurrence_copy": "Colar (instância)",
"cmd.occurrence_ground": "Fixar no solo",
@@ -775,6 +935,7 @@
"cmd.view_left": "Vista esquerda",
"cmd.view_list": "Listar vistas nomeadas",
"cmd.view_navigate": "Navegar na vista",
"cmd.view_normal_to": "Normal à seleção",
"cmd.view_rename": "Renomear vista nomeada",
"cmd.view_right": "Vista direita",
"cmd.view_save": "Nova vista nomeada",
@@ -874,6 +1035,12 @@
"help.licences": "Licenças",
"help.mit": "Licença MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Corta as arestas selecionadas com um bisel plano na distância que você definir. Use para quebrar arestas vivas ou facilitar a entrada de um pino ou parafuso.",
"help.solid.extrude": "Empurra um perfil de esboço para o 3D ao longo de uma distância, adicionando ou removendo material. Use para transformar um contorno plano em um bloco, um ressalto ou um rebaixo.",
"help.solid.fillet": "Arredonda as arestas selecionadas com uma concordância circular suave do raio que você definir. Use para suavizar arestas vivas, por resistência, conforto ou aparência.",
"help.solid.hole": "Faz um furo redondo do diâmetro que você definir em um ponto escolhido de uma face. Use para folgas de parafuso, pinos e outras aberturas redondas simples.",
"help.solid.revolve": "Gira um perfil de esboço em torno de um eixo para criar um sólido redondo. Use para eixos, botões, tigelas e qualquer peça redonda em torno de uma linha central.",
"help.solid.shell": "Esvazia um sólido, deixando paredes da espessura que você definir, e pode abrir as faces que você escolher. Use para caixas, tampas e outras peças de parede fina.",
"help.third_party": "Licenças de terceiros",
"home.back": "Voltar para {name}",
"home.crashes": "Relatórios de falhas",
@@ -978,11 +1145,21 @@
"motion.key_here": "Inserir quadro-chave aqui",
"motion.key_here_hint": "Adicionar um quadro-chave neste passo com o valor atual da junta",
"motion.no_collisions": "Sem colisões durante o estudo",
"motion.plot_quantity": "Quantidade",
"motion.step_back": "Passo anterior",
"motion.step_forward": "Próximo passo",
"motion.to_end": "Para o fim",
"motion.to_start": "Para o início",
"motion.track_name": "Trilha {number}",
"nav.alias.help": "Shift+Alt+arrastar com o esquerdo orbita, Shift+Alt+meio desloca, Shift+Alt+direito aplica zoom; arrastar com o meio também desloca e arrastar com o direito orbita",
"nav.blender.help": "Arrastar com o meio orbita, Shift+meio desloca, Ctrl+meio aplica zoom; arrastar com o direito não mexe na câmera",
"nav.free_orbit.help": "O mouse e o mouse 3D giram passando pelos dois polos e preservam o giro lateral (roll). O encaixe em eixo com Shift fica desativado.",
"nav.free_orbit.label": "Órbita livre",
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.openscad.help": "Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
"nav.solidworks.help": "Arrastar com o meio orbita, Ctrl+meio desloca, Shift+meio aplica zoom, arrastar com o direito orbita",
"nav.tinkercad.help": "Arrastar com o direito orbita, arrastar com o meio ou Shift+direito desloca",
"nav.touchpad.help": "Segure Shift e mova o ponteiro para deslocar; segure Alt/Option e mova para orbitar. Não é preciso botão do mouse. A rolagem vertical aplica zoom.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Orbitar (segure Alt/Option e mova; sem botão do mouse)",
@@ -1005,8 +1182,130 @@
"pattern.flip_direction": "Inverter direção",
"pick a hole to seat it, or OK places it at the origin": "selecione um furo para assentá-la, ou OK a coloca na origem",
"pick one or two vertices, edges or faces": "selecione um ou dois vértices, arestas ou faces",
"prefs.add_item": "Adicionar o item",
"prefs.alt_frees_manipulators": "Alt / Option também libera as setas do manipulador",
"prefs.anti_aliasing_note": "Suaviza bordas serrilhadas.",
"prefs.applies_on_restart": "Aplica-se ao reiniciar",
"prefs.assistant": "Assistente",
"prefs.axes": "Eixos",
"prefs.axis": "Eixo",
"prefs.bottom": "Embaixo",
"prefs.browser": "Navegador",
"prefs.built_in": "Integrado",
"prefs.button_left": "Arrastar com o esquerdo",
"prefs.button_middle": "Arrastar com o meio",
"prefs.button_right": "Arrastar com o direito",
"prefs.choose_font": "Escolher arquivo de fonte…",
"prefs.click": "Clique",
"prefs.contact_shading": "Sombreamento suave de contato",
"prefs.contact_shading_note": "Sombreamento suave onde as peças se aproximam.",
"prefs.context_menu": "Menu de contexto",
"prefs.context_menu_ring": "Menu de contexto · segure para o menu radial",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Abrir relatórios de falhas…",
"prefs.crash_reports_note": "Os relatórios ficam guardados neste computador, com o projeto e os comandos que reproduzem o problema.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Superfícies curvas",
"prefs.curves": "Superfícies curvas",
"prefs.dead_zone_note": "A faixa hachurada é a zona morta: o movimento dentro dela é ignorado.",
"prefs.default": "padrão",
"prefs.design": "Padrões do projeto",
"prefs.design_note": "Com o que os novos projetos começam. Projetos abertos mantêm as próprias configurações.",
"prefs.devices": "Mouse 3D",
"prefs.display": "Exibição",
"prefs.edges": "Arestas e sombreamento",
"prefs.extend": "Extensões",
"prefs.extra_navigation": "Ctrl+arrastar com o esquerdo no espaço vazio orbita; segure Espaço para deslocar.",
"prefs.follows_os": "Segue o sistema",
"prefs.font": "Fonte",
"prefs.font_fallback": "Os glifos que faltam na fonte escolhida usam a fonte integrada, então nenhum rótulo mostra uma caixa vazia.",
"prefs.font_sample": "Vovô viu o ímã, a maçã e o pinguim · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Não foi possível ler a fonte selecionada.",
"prefs.general": "Geral",
"prefs.general_note": "Idioma, salvamento e atualizações.",
"prefs.gesture_orbit": "Orbitar",
"prefs.gesture_pan": "Deslocar",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Gráficos",
"prefs.graphics_note": "Troque velocidade por suavidade. Cada opção mostra seu efeito em uma amostra.",
"prefs.graphics_presets_note": "As predefinições ajustam as opções abaixo em conjunto.",
"prefs.history": "Histórico",
"prefs.hold_stop_snapping": "Segure para desativar o encaixe",
"prefs.icons_only": "Só ícones",
"prefs.imported_file": "Arquivo importado",
"prefs.input": "Entrada",
"prefs.interface": "Interface",
"prefs.interface_language": "Idioma da interface",
"prefs.interface_note": "A aparência do SolidCraft e onde ficam suas partes.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Rótulos",
"prefs.layout": "Layout",
"prefs.layout_note": "Compacto encaixa os painéis nas bordas da janela. A translucidez do navegador está disponível em Flutuante. Mude o lado do Assistente na página Assistente.",
"prefs.left": "Esquerda",
"prefs.left_click": "Clique esquerdo",
"prefs.live_input": "Entrada ao vivo",
"prefs.live_preview": "Visualização ao vivo",
"prefs.minutes": "minutos",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mouse e trackpad",
"prefs.mouse_heading": "Mouse",
"prefs.mouse_mapping": "Mapeamento do mouse",
"prefs.navigation": "Navegação",
"prefs.navigation_note": "Como o mouse, o trackpad e o mouse 3D movem a vista.",
"prefs.not_on_gpu": "indisponível nesta GPU",
"prefs.on": "Ativado",
"prefs.opaque": "Opaco",
"prefs.orbit_snap": "Segure Shift ao orbitar para encaixar em um eixo.",
"prefs.orientation": "Orientação",
"prefs.overall_speed": "Velocidade geral",
"prefs.preview_choices": "Prévia das suas escolhas",
"prefs.quality_default": "padrão",
"prefs.quality_fastest": "mais rápido",
"prefs.quality_slowest": "mais lento",
"prefs.quality_smoother": "mais suave",
"prefs.reports_saved": "{n} salvos",
"prefs.right_click": "Clique direito",
"prefs.saving": "Salvamento automático e recuperação",
"prefs.scale_note": "Também Ctrl + / Ctrl − na janela. Os botões têm 28 px em 100%.",
"prefs.scale_text": "Escala e texto",
"prefs.select": "Selecionar",
"prefs.select_one_item": "Selecionar um item",
"prefs.selecting": "Seleção",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift só adiciona; Ctrl / Cmd alterna",
"prefs.shift_add_note": "Ativado: Shift adiciona, Ctrl / Cmd alterna. Desativado (padrão): ambos alternam.",
"prefs.shortcuts": "Atalhos",
"prefs.sketch_selection": "Esboço e seleção",
"prefs.sketch_selection_heading": "Esboço e seleção",
"prefs.sketch_selection_note": "Como os esboços encaixam e como os cliques selecionam.",
"prefs.sketching": "Criação de esboços",
"prefs.snap_drawing_note": "Ao desenhar ou arrastar um ponto do esboço.",
"prefs.source": "Origem",
"prefs.speed": "Velocidade",
"prefs.stand_up_note": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X.",
"prefs.status_done": "Concluído",
"prefs.status_error": "Erro",
"prefs.status_selected": "Selecionado",
"prefs.status_warning": "Aviso",
"prefs.supersampling_brief": "Desenha a vista maior e a reduz com filtragem: muito nítida, e mais pesada.",
"prefs.tab_accent_note": "A cor de destaque acompanha a aba ativa",
"prefs.theme_file": "arquivo de tema",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "agora",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Adicionar o item, ou retirá-lo de novo",
"prefs.toolbar": "Barra de ferramentas",
"prefs.translucent": "Translúcido",
"prefs.under_browser": "Abaixo do navegador",
"prefs.under_cursor": "Sob o cursor",
"prefs.units_precision": "Unidades e precisão",
"prefs.updates": "Atualizações",
"prefs.view": "Comportamento da vista",
"prefs.viewport": "Janela de visualização",
"prefs.wheel": "Roda",
"prefs.wheel_trackpad": "Roda e trackpad",
"prefs.workspace": "Espaço de trabalho",
"prefs.zoom_toward_cursor": "Zoom em direção ao cursor",
"properties.title": "Propriedades: {name}",
"recovery.days_ago": "há {count} dias",
"recovery.detail": "{file} · {features} recursos · salvo automaticamente {when}",
@@ -1084,9 +1383,12 @@
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
"settings.msaa_reset": "{setting}: {requested}× não é suportado por este dispositivo gráfico; agora é {reset}× (suportados: {supported}).",
"settings.msaa_unsupported": "Não suportado por este dispositivo gráfico",
"settings.reference_geometry_through_bodies": "Mostrar geometria de referência através dos corpos",
"settings.reference_geometry_through_bodies_help": "Desenhar contornos de esboços inativos e planos de referência sobre os corpos sombreados",
"settings.reset_intro": "Estas configurações não funcionam neste computador e impediriam o SolidCraft de iniciar, por isso foram alteradas:",
"settings.reset_title": "As configurações foram alteradas",
"settings.supersampling_note": "Desenha a vista em resolução maior e a filtra para baixo: arestas, linhas e grade mais suaves em telas abaixo de 4K. Custa tempo de GPU: cerca de 2,25× os pixels em 1,5× e 4× em 2×.",
"sheet.no_body": "não há corpo de chapa metálica: comece um com Flange > Base, ou transforme uma placa em chapa metálica com Converter em chapa metálica (selecione a face plana grande dela)",
"shortcut.json_error": "os parâmetros devem estar no formato JSON: {error}",
"shortcut.no_command": "o comando `{id}` não existe; o texto antes do JSON deve ser um ID de comando",
"shortcut.pin": "Fixar no topo da caixa S",
@@ -1123,6 +1425,9 @@
"thread.designation": "Designação",
"thread.iso_metric": "Métrica ISO",
"thread.select_size": "Selecionar tamanho",
"timeline.delete_after_marker_one": "Excluir 1 recurso após o marcador do histórico",
"timeline.delete_after_marker_other": "Excluir {count} recursos após o marcador do histórico",
"timeline.delete_many": "Excluir {count} recursos selecionados",
"timeline.group": "Nova pasta de recursos",
"timeline.move_many": "Mover itens da linha do tempo",
"timeline.move_to_group": "Mover recursos para a pasta",
@@ -1368,11 +1673,15 @@
"ui.agent_chat_waiting": "Aguardando sua resposta",
"ui.agent_cursor": "Cursor do agente",
"ui.agent_cursor_speed": "Velocidade do cursor do agente",
"ui.agent_off_thread": "Manter a janela respondendo enquanto os agentes executam comandos",
"ui.agent_off_thread_note": "Os comandos lentos de um agente (do painel de IA ou de um cliente MCP) são calculados ao lado da janela: você pode continuar girando e inspecionando a vista, e parar um deles pelo indicador na vista. Desativado: cada um roda na thread da janela, e a janela espera ele terminar.",
"ui.ai_add": "Adicionar",
"ui.ai_add_agent": "Adicionar agente…",
"ui.ai_add_agent_title": "Adicionar agente",
"ui.ai_add_anthropic": "API compatível com Anthropic",
"ui.ai_add_api": "API compatível com OpenAI",
"ui.ai_add_custom": "Comando personalizado",
"ui.ai_add_endpoint": "Endpoint de API",
"ui.ai_add_installed": "Agente instalado",
"ui.ai_add_installed_note": "Uma entrada própria para um agente instalado, por exemplo com outros argumentos. Os agentes instalados são listados por si mesmos.",
"ui.ai_added": "adicionado",
@@ -1396,11 +1705,13 @@
"ui.ai_http_busy": "o endpoint está ocupado ou acima do limite; tente de novo mais tarde",
"ui.ai_http_key": "o endpoint recusou a chave: verifique-a em Preferências > IA",
"ui.ai_http_not_found": "endpoint ou modelo inexistente: verifique a URL base e o nome do modelo",
"ui.ai_http_too_large": "a conversa é grande demais para este endpoint; comece um novo chat",
"ui.ai_key": "Chave de API",
"ui.ai_key_hint": "opcional; guardada no chaveiro do sistema",
"ui.ai_key_needs_https": "Uma chave só é enviada por HTTPS ou para este computador.",
"ui.ai_key_note": "Guardada no chaveiro do sistema, nunca nas configurações do SolidCraft. Deixe em branco para um servidor local.",
"ui.ai_keychain_error": "O chaveiro do sistema recusou: {error}",
"ui.ai_kind": "Tipo",
"ui.ai_mode_all": "Permitir tudo",
"ui.ai_mode_all_note": "Nada pergunta antes. Os relatórios de falha continuam fora de alcance e os limites de pasta ainda valem.",
"ui.ai_mode_all_warning": "O agente pode salvar, sobrescrever, exportar e substituir seu projeto sem perguntar. Use isto só com um agente em que você confie e em projetos que você já salvou.",
@@ -1422,6 +1733,7 @@
"ui.ai_panel": "Painel",
"ui.ai_permissions": "Permissões",
"ui.ai_preset": "Predefinição",
"ui.ai_protocol": "Protocolo",
"ui.ai_remove": "Remover",
"ui.ai_reset": "Redefinir",
"ui.ai_settings": "Configurações de IA…",
@@ -1431,6 +1743,8 @@
"ui.ai_test": "Testar",
"ui.ai_test_acp_ok": "Responde: {agent}",
"ui.ai_test_acp_tip": "Inicia o agente por um instante e pede uma sessão; depois o encerra",
"ui.ai_test_answered": "Acessado: {model} respondeu",
"ui.ai_test_anthropic_tip": "Envia uma mensagem curta ao endpoint",
"ui.ai_test_api_tip": "Pede ao endpoint a lista de modelos",
"ui.ai_test_no_model": "Acessado: {count} modelos, mas não {model}",
"ui.ai_test_ok": "Acessado: {count} modelos",
@@ -1589,6 +1903,9 @@
"ui.export_keymap_tip": "Teclas, favoritos da caixa S e a barra de ferramentas, como um arquivo",
"ui.extend": "Estender",
"ui.extent_type": "Tipo de extensão",
"ui.extract_component": "Extrair para arquivo…",
"ui.extract_design_changed": "O projeto mudou enquanto um arquivo era escolhido. Inicie Extrair para arquivo de novo.",
"ui.extract_done": "O componente agora é um projeto próprio, vinculado aqui.",
"ui.extrude": "Extrudar",
"ui.factor": "Fator",
"ui.feature_history": "Histórico de recursos",
@@ -1599,6 +1916,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos ou teclas",
"ui.findInTimeline": "Localizar na linha do tempo",
"ui.findInWindow": "Localizar na janela",
"ui.fine": "Fina",
"ui.finest": "Muito fina",
"ui.finish_sketch": "Concluir esboço",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1687,6 +2006,7 @@
"ui.heading_torus": "TORO",
"ui.heading_utilities": "UTILITÁRIOS",
"ui.heading_utility": "UTILITÁRIO",
"ui.heading_weldments": "SOLDAGEM",
"ui.help": "Ajuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar eixos",
@@ -1696,10 +2016,17 @@
"ui.hideProfile": "Ocultar perfil",
"ui.history_back": "Voltar uma etapa",
"ui.history_below_browser": "Abaixo do navegador",
"ui.history_edge_hint": "clique duas vezes na borda",
"ui.history_first": "Ir para o início",
"ui.history_forward": "Avançar uma etapa",
"ui.history_hide_names": "Ocultar nomes",
"ui.history_last": "Ir para o fim",
"ui.history_marker_tip": "Marcador do histórico: arraste para retroceder o modelo; novos recursos entram aqui",
"ui.history_names": "Nomes",
"ui.history_play": "Reproduzir até o fim",
"ui.history_show_names": "Mostrar nomes",
"ui.history_title": "Histórico",
"ui.history_toggle_names": "Mostrar ou ocultar os nomes dos recursos",
"ui.hole": "Furo",
"ui.hole_tap_type": "Tipo de rosca do furo",
"ui.hole_type": "Tipo de furo",
@@ -1721,6 +2048,10 @@
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
"ui.link_copies": "Vincular cópias idênticas",
"ui.link_copies_keep": "Manter",
"ui.link_copies_link": "Vincular",
"ui.link_copies_text": "Estes componentes constroem os mesmos corpos que {name}. Torná-los posicionamentos do vínculo dele?",
"ui.lock": "Bloquear",
"ui.loft": "Loft",
"ui.look_at": "Olhar para",
@@ -1823,6 +2154,7 @@
"ui.project_face_edges_sketch": "Projetar arestas da face em novos esboços",
"ui.properties": "Propriedades",
"ui.pull_direction": "Direção de extração",
"ui.quality": "Qualidade",
"ui.quantity": "Quantidade",
"ui.quantity_2": "Quantidade 2",
"ui.quit": "Sair",
@@ -1844,6 +2176,7 @@
"ui.render": "Renderizar",
"ui.renderer": "Renderizador",
"ui.repeat": "Repetir",
"ui.replace_with_link_done": "As cópias agora são posicionamentos do projeto vinculado.",
"ui.report_a_bug": "Relatar um bug",
"ui.reset": "Redefinir",
"ui.reset_all": "Redefinir tudo",
@@ -1917,6 +2250,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir corpo",
"ui.stand_up_tip": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X",
"ui.standard": "Padrão",
"ui.start": "Início",
"ui.start_over": "Recomeçar",
"ui.start_page": "Página inicial",
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "Cheie aici",
"motion.key_here_hint": "Adaugă o cheie la acest pas cu valoarea curentă a îmbinării",
"motion.no_collisions": "Nicio coliziune în studiu",
"motion.plot_quantity": "Cantitate",
"motion.step_back": "Pas înapoi",
"motion.step_forward": "Pas înainte",
"motion.to_end": "La sfârșit",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "endpointul este ocupat sau și-a depășit limita; încercați mai târziu",
"ui.ai_http_key": "endpointul a refuzat cheia: verificați-o în Preferințe > AI",
"ui.ai_http_not_found": "nu există un astfel de endpoint sau model: verificați URL-ul de bază și numele modelului",
"ui.ai_http_too_large": "conversația este prea mare pentru acest endpoint; începeți un chat nou",
"ui.ai_key": "Cheie API",
"ui.ai_key_hint": "opțional; păstrată în depozitul de chei al sistemului",
"ui.ai_key_needs_https": "O cheie se trimite doar prin HTTPS sau către acest calculator.",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "Filtrează comenzi sau taste",
"ui.findInTimeline": "Găsește în cronologie",
"ui.findInWindow": "Găsește în fereastră",
"ui.fine": "Fină",
"ui.finest": "Foarte fină",
"ui.finish_sketch": "Finalizează schița",
"ui.fit": "Potrivire",
"ui.fix_unfix": "Fixează/Defixează",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "TOR",
"ui.heading_utilities": "UTILITARE",
"ui.heading_utility": "UTILITAR",
"ui.heading_weldments": "SUDURĂ",
"ui.help": "Ajutor",
"ui.hide": "Ascunde",
"ui.hideAxes": "Ascunde axele",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "Proiectează muchiile feței în schițe noi",
"ui.properties": "Proprietăți",
"ui.pull_direction": "Direcția de extragere",
"ui.quality": "Calitate",
"ui.quantity": "Cantitate",
"ui.quantity_2": "Cantitatea 2",
"ui.quit": "Ieșire",
@@ -1852,6 +1858,7 @@
"ui.spline": "Spline",
"ui.split_body": "Împarte corpul",
"ui.stand_up_tip": "După ce un fișier STEP sau IGES se deschide cu Z în sus, oferă un sfert de rotație în jurul lui X",
"ui.standard": "Standard",
"ui.start": "Început",
"ui.start_over": "Reîncepe",
"ui.start_page": "Pagina de start",
+8 -1
View File
@@ -269,7 +269,7 @@
"Preview Row": "Строка предпросмотра",
"Preview leaves the current design unchanged.": "Предпросмотр не меняет текущий дизайн.",
"Profiles/Curves": "Профили/кривые",
"Quantity": "Величина",
"Quantity": "Количество",
"RECENT": "НЕДАВНИЕ",
"REFERENCE": "ССЫЛОЧНАЯ ГЕОМЕТРИЯ",
"RULED SURFACE": "ЛИНЕЙЧАТАЯ ПОВЕРХНОСТЬ",
@@ -970,6 +970,7 @@
"motion.key_here": "Вставить ключевой кадр здесь",
"motion.key_here_hint": "Добавить ключевой кадр на этом шаге с текущим значением сопряжения",
"motion.no_collisions": "За всё исследование столкновений нет",
"motion.plot_quantity": "Величина",
"motion.step_back": "Шаг назад",
"motion.step_forward": "Шаг вперёд",
"motion.to_end": "В конец",
@@ -1388,6 +1389,7 @@
"ui.ai_http_busy": "конечная точка занята или превысила лимит; повторите позже",
"ui.ai_http_key": "конечная точка отклонила ключ: проверьте его в Настройки > ИИ",
"ui.ai_http_not_found": "нет такой конечной точки или модели: проверьте базовый URL и имя модели",
"ui.ai_http_too_large": "разговор слишком велик для этой конечной точки; начните новый чат",
"ui.ai_key": "Ключ API",
"ui.ai_key_hint": "необязательно; хранится в системной связке ключей",
"ui.ai_key_needs_https": "Ключ отправляется только по HTTPS или на этот компьютер.",
@@ -1591,6 +1593,8 @@
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
"ui.findInTimeline": "Найти на шкале",
"ui.findInWindow": "Найти в окне",
"ui.fine": "Высокое",
"ui.finest": "Максимальное",
"ui.finish_sketch": "Завершить эскиз",
"ui.fit": "Вписать",
"ui.fix_unfix": "Зафиксировать/Освободить",
@@ -1679,6 +1683,7 @@
"ui.heading_torus": "ТОР",
"ui.heading_utilities": "УТИЛИТЫ",
"ui.heading_utility": "УТИЛИТА",
"ui.heading_weldments": "СВАРКА",
"ui.help": "Справка",
"ui.hide": "Скрыть",
"ui.hideAxes": "Скрыть оси",
@@ -1815,6 +1820,7 @@
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
"ui.properties": "Свойства",
"ui.pull_direction": "Направление вытягивания",
"ui.quality": "Качество",
"ui.quantity": "Количество",
"ui.quantity_2": "Количество 2",
"ui.quit": "Выход",
@@ -1909,6 +1915,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Разделить тело",
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
"ui.standard": "Стандартное",
"ui.start": "Старт",
"ui.start_over": "Начать заново",
"ui.start_page": "Начальная страница",
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "Nyckel här",
"motion.key_here_hint": "Lägg till en nyckel vid detta steg med ledens nuvarande värde",
"motion.no_collisions": "Inga kollisioner under studien",
"motion.plot_quantity": "Antal",
"motion.step_back": "Steg bakåt",
"motion.step_forward": "Steg framåt",
"motion.to_end": "Till slutet",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "slutpunkten är upptagen eller över sin gräns; försök igen senare",
"ui.ai_http_key": "slutpunkten nekade nyckeln: kontrollera den under Inställningar > AI",
"ui.ai_http_not_found": "ingen sådan slutpunkt eller modell: kontrollera bas-URL:en och modellnamnet",
"ui.ai_http_too_large": "konversationen är för stor för den här slutpunkten; starta en ny chatt",
"ui.ai_key": "API-nyckel",
"ui.ai_key_hint": "valfritt; lagras i systemets nyckelring",
"ui.ai_key_needs_https": "En nyckel skickas endast via HTTPS, eller till den här datorn.",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "Filtrera kommandon eller tangenter",
"ui.findInTimeline": "Sök i tidslinjen",
"ui.findInWindow": "Sök i fönstret",
"ui.fine": "Fin",
"ui.finest": "Finast",
"ui.finish_sketch": "Avsluta skiss",
"ui.fit": "Anpassa",
"ui.fix_unfix": "Fäst/Lös",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "VERKTYG",
"ui.heading_utility": "VERKTYG",
"ui.heading_weldments": "SVETSNING",
"ui.help": "Hjälp",
"ui.hide": "Dölj",
"ui.hideAxes": "Dölj axlar",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "Projicera ytkanter i nya skisser",
"ui.properties": "Egenskaper",
"ui.pull_direction": "Dragriktning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Avsluta",
@@ -1852,6 +1858,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dela kropp",
"ui.stand_up_tip": "När en STEP- eller IGES-fil öppnas med Z uppåt, erbjud ett kvartsvarv runt X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Börja om",
"ui.start_page": "Startsida",
+8 -1
View File
@@ -269,7 +269,7 @@
"Preview Row": "Satırı önizle",
"Preview leaves the current design unchanged.": "Önizleme geçerli tasarımı değiştirmez.",
"Profiles/Curves": "Profiller/Eğriler",
"Quantity": "Büyüklük",
"Quantity": "Adet",
"RECENT": "SON KULLANILANLAR",
"REFERENCE": "REFERANS",
"RULED SURFACE": "REGLE YÜZEY",
@@ -970,6 +970,7 @@
"motion.key_here": "Buraya anahtar kare ekle",
"motion.key_here_hint": "Bu adıma bağlantının mevcut değeriyle bir anahtar kare ekle",
"motion.no_collisions": "Etüt boyunca çarpışma yok",
"motion.plot_quantity": "Büyüklük",
"motion.step_back": "Bir adım geri",
"motion.step_forward": "Bir adım ileri",
"motion.to_end": "Sona git",
@@ -1388,6 +1389,7 @@
"ui.ai_http_busy": "uç nokta meşgul veya sınırda; daha sonra tekrar deneyin",
"ui.ai_http_key": "uç nokta anahtarı reddetti: Ayarlar > AI altında kontrol edin",
"ui.ai_http_not_found": "Uç nokta veya model bulunamadı: temel URL'yi ve model adını kontrol edin",
"ui.ai_http_too_large": "konuşma bu uç nokta için çok büyük; yeni bir sohbet başlatın",
"ui.ai_key": "API anahtarı",
"ui.ai_key_hint": "isteğe bağlı; sistem anahtar zincirinde saklanır",
"ui.ai_key_needs_https": "Anahtar yalnızca HTTPS üzerinden veya bu bilgisayara gönderilir.",
@@ -1591,6 +1593,8 @@
"ui.filter_commands_or_keys": "Komutları veya tuşları filtrele",
"ui.findInTimeline": "Zaman çizelgesinde bul",
"ui.findInWindow": "Pencerede bul",
"ui.fine": "İnce",
"ui.finest": "En ince",
"ui.finish_sketch": "Eskizi bitir",
"ui.fit": "Sığdır",
"ui.fix_unfix": "Sabitle/Çöz",
@@ -1679,6 +1683,7 @@
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "ARAÇLAR",
"ui.heading_utility": "YARDIMCI ARAÇLAR",
"ui.heading_weldments": "KAYNAK",
"ui.help": "Yardım",
"ui.hide": "Gizle",
"ui.hideAxes": "Eksenleri gizle",
@@ -1815,6 +1820,7 @@
"ui.project_face_edges_sketch": "Yüz kenarlarını yeni eskizlere yansıt",
"ui.properties": "Özellikler",
"ui.pull_direction": "Kalıptan çıkarma yönü",
"ui.quality": "Kalite",
"ui.quantity": "Adet",
"ui.quantity_2": "Adet 2",
"ui.quit": "Çıkış",
@@ -1909,6 +1915,7 @@
"ui.spline": "Spline",
"ui.split_body": "Gövdeyi böl",
"ui.stand_up_tip": "Bir STEP veya IGES dosyası Z yukarı olarak açıldığında X etrafında çeyrek tur döndürmeyi öner",
"ui.standard": "Standart",
"ui.start": "Başlangıç",
"ui.start_over": "Baştan başla",
"ui.start_page": "Başlangıç sayfası",
+311 -2
View File
@@ -14,11 +14,15 @@
"Active": "Активна",
"Active rule": "Активне правило",
"Add": "Додати",
"Add Key": "Додати клавішу",
"Add Column": "Додати стовпець",
"Add Key": "Додати ключовий кадр",
"Add Mid Radius": "Додати середній радіус",
"Add Row": "Додати рядок",
"Add a tool": "Додати інструмент",
"Add condition": "Додати умову",
"Add or edit rule": "Додати або змінити правило",
"Add to New Folder": "Додати до нової теки",
"Advanced": "Додатково",
"Against": "Проти",
"Alignment": "Вирівнювання",
"All Sketch Points": "Усі точки ескізу",
@@ -28,9 +32,11 @@
"Also deletes": "Також видаляє",
"Angle 2": "Кут 2",
"Animate": "Анімувати",
"App/system information and command settings are included. Review private details before sharing.": "Додано відомості про програму й систему та налаштування команд. Перегляньте приватні дані перед поширенням.",
"Appearance name": "Назва вигляду",
"Applied Radius": "Застосований радіус",
"Apply To": "Застосувати до",
"Apply library appearance": "Застосувати вигляд із бібліотеки",
"Apply to selection": "Застосувати до виділення",
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Спочатку застосуйте вигляд. Експортує призначені вигляди та матеріали, створені в цьому проєкті.",
"Arc length": "Довжина дуги",
@@ -41,13 +47,18 @@
"BLEND SURFACE": "ПОВЕРХНЯ ПЕРЕХОДУ",
"BOUNDARY SURFACE": "ПОВЕРХНЯ ЗА МЕЖАМИ",
"BROWSER": "БРАУЗЕР",
"Bend Edges": "Ребра згину",
"Bend Line Position": "Положення лінії згину",
"Bend Position": "Положення згину",
"Bend Radius": "Радіус згину",
"Bend Table": "Таблиця згинів",
"Bends": "Згини",
"Bodies": "Тіла",
"Bodies (optional)": "Тіла (необов'язково)",
"Body": "Тіло",
"Body (last if none)": "Тіло (останнє, якщо не вибрано)",
"Body to split": "Тіло для розділення",
"Body, or faces to take out": "Тіло або грані для вилучення",
"Boundary": "Межа",
"Bounding box": "Обмежувальна рамка",
"CHECK": "ПЕРЕВІРКА",
@@ -59,7 +70,9 @@
"CSV or JSON: updates existing parameters, adds new ones": "CSV або JSON: оновлює наявні параметри, додає нові",
"Capture All Positions": "Зафіксувати всі положення",
"Capture Pos.": "Зафікс. полож.",
"Catch Depth": "Глибина зачеплення",
"Catch Direction": "Напрямок зачеплення",
"Catch Length": "Довжина зачеплення",
"Cells": "Комірки",
"Center X": "Центр X",
"Center Y": "Центр Y",
@@ -68,23 +81,31 @@
"Chamfer Type": "Тип фаски",
"Check collisions": "Перевіряти зіткнення",
"Checked": "Перевірено",
"Choose Variable radius or Spline in Type.": "Виберіть у полі «Тип» значення «Variable radius» або «Spline».",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Виберіть нову площину. Геометрія та розміри ескізу залишаються незмінними у своїх двовимірних координатах.",
"Choose…": "Вибрати…",
"Circular": "Круговий",
"Circumference": "Довжина кола",
"Clockwise": "За годинниковою стрілкою",
"Close draft": "Закрити чернетку",
"Collapse": "Згорнути",
"Collisions": "Зіткнення",
"Colour": "Колір",
"Command": "Команда",
"Comment": "Коментар",
"Component 1": "Компонент 1",
"Component 2": "Компонент 2",
"Component1": "Компонент1",
"Components": "Компоненти",
"Config Table": "Табл. конфігурацій",
"Configuration": "Конфігурація",
"Configuration rules": "Правила конфігурації",
"Contact": "Контакт",
"Contact Sets": "Набори контактів",
"Contact sets": "Набори контактів",
"Continue on GitHub": "Продовжити на GitHub",
"Continuity": "Неперервність",
"Control Radius": "Керівний радіус",
"Copy Error Message": "Копіювати повідомлення про помилку",
"Copy Warning Message": "Копіювати попереджувальне повідомлення",
"Copy all licences": "Копіювати всі ліцензії",
@@ -94,8 +115,11 @@
"Corner Relief": "Полегшення кута",
"Corner Relief Shape": "Форма полегшення кута",
"Counterbore depth": "Глибина циліндричного розширення",
"Counterbore Ø": "Ø циліндричного розширення",
"Countersink angle": "Кут зенкування",
"Countersink Ø": "Ø зенкування",
"Create Issue — review draft": "Створити повідомлення про проблему — перегляд чернетки",
"Create Issue…": "Створити повідомлення про проблему…",
"Cross curves": "Поперечні криві",
"Cube": "Куб",
"Current orthographic view (1:1 mm)": "Поточний ортографічний вид (1:1 mm)",
@@ -103,18 +127,28 @@
"Curv. Comb": "Гребінь кривини",
"Curvature (G2)": "Кривина (G2)",
"Curvature change (G3)": "Зміна кривини (G3)",
"Custom": "Власний",
"Customize Toolbar": "Налаштувати панель інструментів",
"Customize…": "Налаштувати…",
"Cylindrical face": "Циліндрична грань",
"Delete": "Видалити",
"Delete Selected Features…": "Видалити вибрані операції…",
"Delete Selected Folders": "Видалити вибрані теки",
"Delete all": "Видалити все",
"Delete rule": "Видалити правило",
"Delete the row": "Видалити рядок",
"Delete the set": "Видалити набір",
"Density": "Густина",
"Dependents": "Залежні",
"Describe the problem you saw": "Опишіть проблему, яку ви побачили",
"Describe what happened before continuing.": "Опишіть, що сталося, перш ніж продовжити.",
"Designs you open or save appear here.": "Проєкти, які ви відкриваєте чи зберігаєте, з'являються тут.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Визначається об'єктом призначення. Виберіть «Кут», щоб редагувати кутову протяжність.",
"Diameter": "Діаметр",
"Direction": "Напрямок",
"Discard": "Відхилити",
"Distance": "Відстань",
"Distance 1": "Відстань 1",
"Do not export hidden edges": "Не експортувати приховані ребра",
"Don't ask again": "Більше не питати",
"Double-click to rename": "Двічі клацніть, щоб перейменувати",
@@ -126,17 +160,22 @@
"Edit Profile Sketch": "Редагувати ескіз профілю",
"Enable rule": "Увімкнути правило",
"Enabled": "Увімкнено",
"End Radius": "Кінцевий радіус",
"End bulge": "Роздуття кінця",
"End continuity": "Неперервність кінця",
"Expand": "Розгорнути",
"Expected result (optional)": "Очікуваний результат (необов'язково)",
"Explode": "Рознести",
"Explode Text": "Розібрати текст",
"Export Animation…": "Експортувати анімацію…",
"Export Bend Table…": "Експортувати таблицю згинів…",
"Export CSV…": "Експорт CSV…",
"Export Current View as DXF": "Експортувати поточний вид як DXF",
"Export Current View as DXF…": "Експортувати поточний вид як DXF…",
"Export DXF": "Експорт DXF",
"Export DXF…": "Експорт DXF…",
"Export JSON…": "Експорт JSON…",
"Export design library...": "Експортувати бібліотеку проєкту...",
"Export hidden edges as dashed lines": "Експортувати приховані ребра як штрихові лінії",
"Expression": "Вираз",
"Extend to meet": "Подовжити до перетину",
@@ -144,31 +183,41 @@
"FILES": "ФАЙЛИ",
"FLAT": "РОЗГОРТКА",
"Face": "Грань",
"Face Appearance": "Вигляд грані",
"Face attachment lost": "Втрачено прив'язку до грані",
"Faces": "Грані",
"Faces / edges / vertices": "Грані / ребра / вершини",
"Faces to split": "Грані для розділення",
"Fastener": "Кріплення",
"Favorites": "Обране",
"Favourite": "Обране",
"Feature": "Операція",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Файли з багатьох CAD-програм мають напрямок Y вгору, тому тут вони відкриваються лежачи на спині (Z вгору).",
"Filter": "Фільтр",
"Filter…": "Фільтр…",
"Find in Browser": "Знайти в браузері",
"Finish": "Оздоблення",
"First plane": "Перша площина",
"Fit and G2 corners require Constant radius.": "Для підгонки та кутів G2 потрібен тип «Constant radius».",
"Fixed Face": "Фіксована грань",
"Flat Pattern": "Розгортка",
"Flat size": "Розмір розгортки",
"Follow camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
"Follow the camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
"Font": "Шрифт",
"From": "Від",
"From Bodies": "З тіл",
"From edges": "Початкові ребра",
"Full Length": "Повна довжина",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "Кути G2 потребують постійного радіуса та підгонки «Exact». Побудова кутів G2 ще не підтримується.",
"Gap": "Зазор",
"Grid": "Сітка",
"HISTORY": "ІСТОРІЯ",
"Height": "Висота",
"Hidden edges": "Приховані ребра",
"History layout": "Розташування історії",
"History names": "Назви в історії",
"Hole (optional)": "Отвір (необов'язково)",
"Holes": "Отвори",
"Holes (edges on them)": "Отвори (ребра на них)",
"Home view": "Головний вид",
@@ -179,14 +228,20 @@
"In place (surfaces)": "На місці (поверхні)",
"In plane": "У площині",
"Inside": "Усередину",
"Instant": "Миттєво",
"Interface font": "Шрифт інтерфейсу",
"Interferes": "Перетинається",
"Invert": "Інвертувати",
"Isocurve": "Ізокрива",
"Issue title": "Заголовок повідомлення",
"Items": "Елементи",
"JOINTS": "З’ЄДНАННЯ",
"Joint Origin1": "Початок з'єднання1",
"Keep (points on it)": "Залишити (точки на ньому)",
"Keep Tools": "Зберегти інструменти",
"Keep of the other surface": "Залишити від іншої поверхні",
"Key a joint's value at this step": "Вставити ключовий кадр зі значенням з'єднання на цьому кроці",
"Keys": "Ключові кадри",
"LIBRARY": "БІБЛІОТЕКА",
"LINKS": "ПОСИЛАННЯ",
"Later": "Пізніше",
@@ -197,9 +252,14 @@
"Letter Spacing": "Міжлітерний інтервал",
"Library": "Бібліотека",
"Library file": "Файл бібліотеки",
"Limits": "Межі",
"Line Spacing": "Міжрядковий інтервал",
"Line Type": "Тип лінії",
"Listing the installed fonts…": "Пошук встановлених шрифтів…",
"Load library": "Завантажити бібліотеку",
"Local library": "Локальна бібліотека",
"Loop": "Контур",
"M5 or M5x0.5": "M5 або M5x0.5",
"MANAGE": "КЕРУВАННЯ",
"MATERIALS": "МАТЕРІАЛИ",
"MODEL": "МОДЕЛЬ",
@@ -210,14 +270,20 @@
"Matched restrictions check the final values.": "Обмеження, умови яких виконано, перевіряють кінцеві значення.",
"Material": "Матеріал",
"Max": "Макс.",
"Maximum": "Максимум",
"Mesh body": "Тіло сітки",
"Mid Radius": "Середній радіус",
"Middle double-click": "Подвійне клацання середньою кнопкою",
"Min": "Мін.",
"Minimum": "Мінімум",
"Mirror Plane": "Площина дзеркала",
"Mitre": "Скіс",
"Model Parameters": "Параметри моделі",
"Modeled": "Змодельовано",
"More toolbar panels": "Більше панелей інструментів",
"Motion": "Рух",
"Mouse gestures": "Жести мишею",
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Жести мишею працюють лише в режимі миші SolidWorks (колесо обертає, Ctrl + колесо панорамує).",
"Move / Copy": "Перемістити / копіювати",
"Move Type": "Тип переміщення",
"Move down": "Перемістити вниз",
@@ -235,15 +301,22 @@
"No matching appearance presets.": "Немає відповідних наборів вигляду.",
"No matching appearances": "Немає відповідних виглядів",
"No visible bodies or sketches to export.": "Немає видимих тіл або ескізів для експорту.",
"Nominal diameter": "Номінальний діаметр",
"None": "Немає",
"Not available yet": "Ще недоступно",
"Notch": "Виріз",
"OUTPUT": "ВИВЕДЕННЯ",
"Of": "Ступінь вільності",
"On your computer": "На цьому комп'ютері",
"Opacity": "Непрозорість",
"Open GitHub draft": "Відкрити чернетку GitHub",
"Open library...": "Відкрити бібліотеку...",
"Open local libraries in the desktop app. You can export this design's library here.": "Відкривайте локальні бібліотеки в настільному застосунку. Тут можна експортувати бібліотеку цього проєкту.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Параметричний 3D-CAD з відкритим вихідним кодом на Rust. Ліцензовано за MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "Відкриває браузер і копіює текст звіту, щоб ви вставили його на GitHub.",
"Opens your browser. Review and submit your report on GitHub.": "Відкриває браузер. Перегляньте й надішліть звіт на GitHub.",
"Operator": "Оператор",
"Orbit (right drag)": "Обертання (перетягування правою кнопкою)",
"Other side": "Інший бік",
"Other surface (mutual trim)": "Інша поверхня (взаємне обрізання)",
"Outside": "Назовні",
@@ -258,8 +331,11 @@
"Pick the tool bodies": "Виберіть тіла інструмента",
"Picked": "Вибрано",
"Picked point": "Вибрана точка",
"Pitch": "Крок",
"Pivot": "Точка обертання",
"Place at the origin instead": "Натомість розмістити в початку координат",
"Plastic Rules": "Правила пластику",
"Play the joint's motion in the view": "Відтворити рух з'єднання у виді",
"Playback": "Відтворення",
"Plot": "Графік",
"Position": "Положення",
@@ -267,14 +343,18 @@
"Position (G0)": "Положення (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "Положення вздовж ребра: 0 на початку, 1 на іншому кінці",
"Preferences › Design turns the offer back on": "Налаштування › Дизайн повертає пропозицію",
"Preview": "Попередній перегляд",
"Preview Row": "Рядок перегляду",
"Preview leaves the current design unchanged.": "Попередній перегляд не змінює поточний проєкт.",
"Profile / Edges": "Профіль / ребра",
"Profiles or curves": "Профілі або криві",
"Profiles/Curves": "Профілі/криві",
"Quantity": "Величина",
"Quantity": "Кількість",
"RECENT": "НЕДАВНІ",
"REFERENCE": "ДОПОМІЖНА ГЕОМЕТРІЯ",
"RULED SURFACE": "ЛІНІЙЧАСТА ПОВЕРХНЯ",
"RULES": "ПРАВИЛА",
"Radius": "Радіус",
"Radius Fit": "Підгонка за радіусом",
"Ratio": "Співвідношення",
"Reattach selected face": "Повторно приєднати вибрану грань",
@@ -283,6 +363,8 @@
"Rectangular": "Прямокутний",
"Reference face": "Опорна грань",
"Remove Tools": "Прибрати інструменти",
"Remove condition": "Прибрати умову",
"Remove from Folder": "Прибрати з теки",
"Remove from list": "Прибрати зі списку",
"Remove override": "Прибрати перевизначення",
"Remove private design details or paths before sharing.": "Перед публікацією приберіть приватні дані проєкту або шляхи.",
@@ -290,12 +372,18 @@
"Remove the column": "Прибрати стовпець",
"Remove the track": "Прибрати доріжку",
"Remove this point": "Прибрати цю точку",
"Rename Feature": "Перейменувати операцію",
"Rename Folder": "Перейменувати теку",
"Replace existing library file": "Замінити наявний файл бібліотеки",
"Report a bug — review a screenshot and open a GitHub draft": "Повідомити про помилку — переглянути знімок екрана й відкрити чернетку GitHub",
"Report text copied. Paste it into GitHub and submit your report there.": "Текст звіту скопійовано. Вставте його на GitHub і надішліть звіт там.",
"Reports are public. A GitHub account is required.": "Звіти загальнодоступні. Потрібен обліковий запис GitHub.",
"Reset Toolbar": "Скинути панель інструментів",
"Resize Part": "Змінити розмір деталі",
"Restore View": "Відновити вид",
"Restore outer boundary": "Відновити зовнішню межу",
"Restore the inherited component or physical material appearance": "Відновити успадкований вигляд компонента або фізичного матеріалу",
"Restore this studio's tools": "Відновити інструменти цієї студії",
"Result": "Результат",
"Retake screenshot": "Повторити знімок екрана",
"Reverse": "Обернути",
@@ -305,7 +393,14 @@
"Revert Pos.": "Скасув. полож.",
"Revert Position": "Повернути положення",
"Review the report and screenshot before opening GitHub.": "Перегляньте звіт і знімок екрана перед відкриттям GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Перегляньте звіт на GitHub і натисніть Submit new issue, щоб опублікувати його.",
"Rib Length": "Довжина ребра",
"Rib Offset": "Зсув ребра",
"Rib Thickness": "Товщина ребра",
"Ribs": "Ребра",
"Rim Edge": "Крайове ребро",
"Ring of tools on the right button": "Кільце інструментів на правій кнопці",
"Roll Back Here": "Відкотити сюди",
"Roll History Marker Here": "Перемістити маркер історії сюди",
"Round": "Круглий",
"Rule": "Правило",
@@ -315,21 +410,28 @@
"SURFACE QUALITY": "ЯКІСТЬ ПОВЕРХНІ",
"Save Rule": "Зберегти правило",
"Save View": "Зберегти вид",
"Save screenshot…": "Зберегти знімок екрана…",
"Save the study as an animated GIF, seen as the viewport shows it": "Зберегти дослідження як анімований GIF, як його показує вікно перегляду",
"Saved preview. Geometry is being checked before editing.": "Збережений перегляд. Геометрію перевіряють перед редагуванням.",
"Scale factor": "Коефіцієнт масштабу",
"Scene": "Сцена",
"Screenshot (optional)": "Знімок екрана (необов'язково)",
"Search": "Пошук",
"Search commands": "Пошук команд",
"Search this library": "Пошук у цій бібліотеці",
"Second plane": "Друга площина",
"Section": "Переріз",
"Section position": "Положення перерізу",
"Sections (one sketch each)": "Перерізи (по одному ескізу)",
"Select Other": "Вибрати інше",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Вибір іншого: утримуйте ліву кнопку нерухомо на моделі (або клацніть правою › Вибрати інше…), щоб побачити список усього під курсором; клацніть ще раз у тому ж місці, щоб вибрати наступний об'єкт там. Двічі клацніть середньою кнопкою, щоб перейти до головного виду (Налаштування › Навігація).",
"Select bodies (a face picks its body)": "Виділяти тіла (грань виділяє своє тіло)",
"Select bodies or faces instead": "Натомість виділіть тіла або грані",
"Select edges": "Виберіть ребра",
"Selection filter": "Фільтр виділення",
"Semantic faces share their appearance across instances.": "Семантичні грані спільно використовують свій вигляд між екземплярами.",
"Send your report on GitHub": "Надіслати звіт на GitHub",
"Shaded": "Затінений",
"Shape": "Форма",
"Sheet Rules": "Правила листа",
"Show Agent Cursor": "Показати курсор агента",
@@ -348,6 +450,7 @@
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрився до того, як ці проєкти було збережено. Можна відкрити їхнє останнє автозбереження.",
"Specialise": "Спеціалізувати",
"Speed": "Швидкість",
"Splitting tool": "Інструмент розділення",
"Square": "Квадрат",
"Stand it up": "Вирівняти",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Вирівняти: поворот на чверть оберту навколо X як одна операція переміщення, яку можна скасувати.",
@@ -357,17 +460,27 @@
"Start continuity": "Неперервність початку",
"Step": "Крок",
"Steps": "Кроки",
"Steps to reproduce (optional)": "Кроки відтворення (необов'язково)",
"Stitch the kept pieces": "Зшити залишені частини",
"Stitch to both bodies": "Зшити з обома тілами",
"Studio": "Студія",
"Study": "Дослідження",
"Style": "Стиль",
"Summary": "Короткий опис",
"Suppress": "Придушити",
"Suppressed": "Придушено",
"Surface": "Поверхня",
"Surface, or faces to turn over": "Поверхня або грані, щоб обернути нормаль",
"Swap Ends": "Поміняти кінці місцями",
"TRIM": "ОБРІЗАННЯ",
"Tangent (G1)": "Дотик (G1)",
"Tangent Chain": "Дотичний ланцюг",
"Tangent chain": "Дотичний ланцюг",
"Tap drill diameter": "Діаметр свердла під різьбу",
"Target": "Цільова грань або площина",
"Target Body": "Цільове тіло",
"Tear": "Розрив",
"Technical details": "Технічні подробиці",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Штриховий ескіз — це збережений орієнтир площини. Виберіть нову грань, щоб відновити прив'язку.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Збережений вихідний екземпляр відсутній. Перед повторною прив'язкою виберіть дійсне вихідне розміщення.",
"Then": "Потім",
@@ -379,24 +492,32 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Цей дочірній об'єкт спільний. Його видалення прибирає його з кожної копії батьківського.",
"This child is shared. Position changes apply to every copy of its parent.": "Цей дочірній об'єкт спільний. Зміни положення застосовуються до кожної копії батьківського.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ця грань не може забезпечити надійну прив'язку. Виберіть іншу грань або явно використайте фіксовану площину.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Це зображення ще не додано до GitHub. Скопіюйте або збережіть його, а потім додайте на GitHub.",
"To": "До",
"To Offset": "До зсуву",
"To edges": "Кінцеві ребра",
"Tolerance": "Допуск",
"Too close": "Занадто близько",
"Tool Bodies": "Тіла інструмента",
"Tools": "Інструменти",
"Tools, clockwise from the top": "Інструменти за годинниковою стрілкою згори",
"Trimming tool (plane, face or curves)": "Інструмент обрізання (площина, грань або криві)",
"Turn off section analysis before exporting the view.": "Вимкніть аналіз перерізу перед експортом виду.",
"Type": "Тип",
"UNTRIM": "ЗНЯТИ ОБРІЗАННЯ",
"Unit": "Одиниця",
"Unsuppress": "Скасувати придушення",
"Updating the preview": "Оновлення попереднього перегляду",
"Use active author placement": "Використати активне вихідне розміщення",
"Use fixed plane": "Використати фіксовану площину",
"Use saved fixed plane": "Використати збережену фіксовану площину",
"Use selected face as fixed plane": "Використати вибрану грань як фіксовану площину",
"Use the SolidWorks mouse mode": "Використовувати режим миші SolidWorks",
"User Parameters": "Параметри користувача",
"VIEW": "ВИД",
"VIEWS": "ВИГЛЯДИ",
"Value": "Значення",
"Variable Radius": "Змінний радіус",
"Velocity": "Швидкість",
"Vertices": "Вершини",
"View navigator": "Навігатор виду",
@@ -404,16 +525,50 @@
"Wall": "Стінка",
"Wall Location": "Розташування стінки",
"Wall Thickness": "Товщина стінки",
"What did you do just before the problem? A short list is enough": "Що ви робили безпосередньо перед проблемою? Короткого переліку достатньо",
"What did you expect to see?": "Що ви очікували побачити?",
"What happened?": "Що сталося?",
"When": "Коли",
"Width": "Ширина",
"Will fail": "Зазнає невдачі",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Коли жести ввімкнено, права кнопка більше не обертає вид: утримуйте її та рухайте, щоб відкрити кільце, а потім наведіть на зону інструмента. Звичайне клацання правою кнопкою так само відкриває меню.",
"X": "X",
"X Angle": "Кут X",
"X angle": "Кут X",
"X distance": "Відстань X",
"Y": "Y",
"Y Angle": "Кут Y",
"Y angle": "Кут Y",
"Y distance": "Відстань Y",
"Z": "Z",
"Z Angle": "Кут Z",
"Z Distance": "Відстань Z",
"Z angle": "Кут Z",
"Z distance": "Відстань Z",
"Zebra": "Зебра",
"ai.prefs.availability": "Керування асистентом",
"ai.prefs.background_commands": "Виконувати команди агента у фоновому режимі",
"ai.prefs.follow_camera": "Повертати камеру до ескізів",
"ai.prefs.follow_camera_off": "Автоматичне стеження камери вимкнено.",
"ai.prefs.follow_camera_on": "Камера повертається до ескізів, які створює агент.",
"ai.prefs.intro": "Агенти, дозволи та те, як показується їхня робота.",
"ai.prefs.make_default": "Використовувати за замовчуванням",
"ai.prefs.no_agents": "Агентів ще немає. Додайте агента або оновіть список, щоб знайти встановлених.",
"ai.prefs.no_saved_approvals": "Немає збережених дозволів.",
"ai.prefs.override_all_warning": "Цей агент може зберігати, перезаписувати, експортувати або замінювати проєкт без запиту.",
"ai.prefs.permission_all": "Дозволяти все",
"ai.prefs.permission_all_summary": "Доступні інструменти SolidCraft працюють без запиту.",
"ai.prefs.permission_ask": "Запитувати перед змінами",
"ai.prefs.permission_ask_summary": "Підтверджувати кожну зміну проєкту та дію з файлами.",
"ai.prefs.permission_edits": "Дозволяти редагування моделі",
"ai.prefs.permission_edits_summary": "Редагування моделі виконується; для дій з файлами й руйнівних дій усе ще запитують дозвіл.",
"ai.prefs.permission_scope": "Лише для інструментів SolidCraft. Оболонка агента та редагування файлів підкоряються його власним дозволам.",
"ai.prefs.pointer_preview_hidden": "Вказівник приховано",
"ai.prefs.pointer_preview_speed": "Вказівник агента · {speed}",
"ai.prefs.pointer_speed": "Швидкість руху",
"ai.prefs.privacy_note": "Розмову отримує лише вибраний вами агент або кінцева точка. Ключі кінцевих точок зберігаються в системному сховищі ключів.",
"ai.prefs.show_pointer": "Показати вказівник агента",
"ai.prefs.turn_limit_hint": "Максимальна кількість викликів інструментів, що змінюють проєкт, які Асистент може зробити за одне повідомлення.",
"angle, height": "кут, висота",
"appearance.dielectric_ior": "Діелектричний IOR",
"appearance.metallic": "Металевість",
@@ -770,6 +925,7 @@
"cmd.view_graphics_preset": "Набір графіки",
"cmd.view_list": "Список іменованих видів",
"cmd.view_navigate": "Навігація видом",
"cmd.view_normal_to": "Перпендикулярно до виділення",
"cmd.view_rename": "Перейменувати іменований вид",
"cmd.view_save": "Новий іменований вид",
"color.copy": "Клацніть, щоб скопіювати значення кольору",
@@ -867,6 +1023,12 @@
"help.licences": "Ліцензії",
"help.mit": "Ліцензія MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Зрізає вибрані ребра плоским скосом на заданій відстані. Використовуйте, щоб притупити гострі ребра або полегшити вставляння штифта чи гвинта.",
"help.solid.extrude": "Витягує профіль ескізу в 3D на задану відстань, додаючи або видаляючи матеріал. Використовуйте, щоб перетворити плоский контур на брусок, бобишку або кишеню.",
"help.solid.fillet": "Закруглює вибрані ребра плавним круговим переходом заданого радіуса. Використовуйте, щоб пом'якшити гострі ребра для міцності, зручності чи вигляду.",
"help.solid.hole": "Просвердлює круглий отвір заданого діаметра в обраній точці на грані. Використовуйте для отворів під гвинти, штифтів та інших простих круглих отворів.",
"help.solid.revolve": "Обертає профіль ескізу навколо осі, утворюючи кругле тіло. Використовуйте для валів, ручок, чаш та будь-яких деталей, круглих навколо осьової лінії.",
"help.solid.shell": "Робить тіло порожнистим, залишаючи стінки заданої товщини, і може відкривати вибрані грані. Використовуйте для коробок, кришок та інших тонкостінних деталей.",
"help.third_party": "Ліцензії сторонніх компонентів",
"home.back": "Назад до {name}",
"home.crashes": "Звіти про збої",
@@ -971,11 +1133,21 @@
"motion.key_here": "Вставити ключовий кадр тут",
"motion.key_here_hint": "Додати ключовий кадр на цьому кроці з поточним значенням з’єднання",
"motion.no_collisions": "За все дослідження зіткнень немає",
"motion.plot_quantity": "Величина",
"motion.step_back": "Крок назад",
"motion.step_forward": "Крок уперед",
"motion.to_end": "У кінець",
"motion.to_start": "На початок",
"motion.track_name": "Доріжка {number}",
"nav.alias.help": "Shift+Alt+ліва обертає, Shift+Alt+середня панорамує, Shift+Alt+права масштабує; середня також панорамує, права обертає",
"nav.blender.help": "Середня обертає, Shift+середня панорамує, Ctrl+середня масштабує; права не рухає камеру",
"nav.free_orbit.help": "Миша та 3D-миша обертають через обидва полюси й зберігають крен. Прив'язування до осей через Shift вимкнено.",
"nav.free_orbit.label": "Вільне обертання",
"nav.fusion.help": "Середня панорамує, Shift+середня обертає, права обертає",
"nav.inventor.help": "Середня панорамує, Shift+середня обертає, Ctrl+середня масштабує, права обертає",
"nav.openscad.help": "Ліва обертає по порожньому місцю, Shift+ліва виділяє рамкою, геометрію ескізу перетягують для редагування; права або середня панорамує, Shift+права або Shift+середня масштабує",
"nav.solidworks.help": "Середня обертає, Ctrl+середня панорамує, Shift+середня масштабує, права обертає",
"nav.tinkercad.help": "Права обертає, середня або Shift+права панорамує",
"nav.touchpad.help": "Утримуйте Shift і рухайте вказівник, щоб панорамувати; утримуйте Alt/Option і рухайте, щоб обертати. Кнопка миші не потрібна. Вертикальна прокрутка масштабує.",
"nav.touchpad.label": "Трекпад",
"nav.touchpad.orbit": "Обертання (утримуйте Alt/Option і рухайте; без кнопки миші)",
@@ -998,8 +1170,130 @@
"pattern.flip_direction": "Змінити напрямок",
"pick a hole to seat it, or OK places it at the origin": "виберіть отвір для посадки, або OK розмістить його в початку координат",
"pick one or two vertices, edges or faces": "виберіть одну-дві вершини, ребра або грані",
"prefs.add_item": "Додати елемент",
"prefs.alt_frees_manipulators": "Alt / Option також дає вільне перетягування стрілок маніпулятора",
"prefs.anti_aliasing_note": "Згладжує нерівні краї.",
"prefs.applies_on_restart": "Застосовується після перезапуску",
"prefs.assistant": "Асистент",
"prefs.axes": "Осі",
"prefs.axis": "Вісь",
"prefs.bottom": "Низ",
"prefs.browser": "Браузер",
"prefs.built_in": "Вбудована",
"prefs.button_left": "Перетягування лівою кнопкою",
"prefs.button_middle": "Перетягування середньою кнопкою",
"prefs.button_right": "Перетягування правою кнопкою",
"prefs.choose_font": "Вибрати файл шрифту…",
"prefs.click": "Клацання",
"prefs.contact_shading": "М'яке контактне затінення",
"prefs.contact_shading_note": "М'яке затінення там, де деталі зближуються.",
"prefs.context_menu": "Контекстне меню",
"prefs.context_menu_ring": "Контекстне меню · утримуйте для кругового меню",
"prefs.cpu": "ЦП (програмний)",
"prefs.crash_reports": "Відкрити звіти про збої…",
"prefs.crash_reports_note": "Звіти зберігаються на цьому комп'ютері разом із проєктом і командами, що відтворюють проблему.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Криві поверхні",
"prefs.curves": "Криві поверхні",
"prefs.dead_zone_note": "Заштрихована смуга — це мертва зона: рух у її межах ігнорується.",
"prefs.default": "типово",
"prefs.design": "Типові параметри проєкту",
"prefs.design_note": "З чого починаються нові проєкти. Відкриті проєкти зберігають власні параметри.",
"prefs.devices": "3D-миша",
"prefs.display": "Відображення",
"prefs.edges": "Ребра та затінення",
"prefs.extend": "Розширення",
"prefs.extra_navigation": "Ctrl+перетягування лівою на порожньому місці обертає; утримуйте Space, щоб панорамувати.",
"prefs.follows_os": "Як у системі",
"prefs.font": "Шрифт",
"prefs.font_fallback": "Символи, яких немає у вибраному шрифті, беруться з вбудованого, тож жоден напис не показує порожній квадрат.",
"prefs.font_sample": "Ґедзь, що їв фінік, з'їв і юшку · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Не вдалося прочитати вибраний шрифт.",
"prefs.general": "Загальні",
"prefs.general_note": "Мова, збереження та оновлення.",
"prefs.gesture_orbit": "Обертання",
"prefs.gesture_pan": "Панорамування",
"prefs.gesture_zoom": "Масштаб",
"prefs.graphics": "Графіка",
"prefs.graphics_note": "Обміняйте швидкість на плавність. Кожен варіант показує свій ефект на зразку.",
"prefs.graphics_presets_note": "Набори задають наведені нижче параметри разом.",
"prefs.history": "Історія",
"prefs.hold_stop_snapping": "Утримуйте, щоб вимкнути прив'язування",
"prefs.icons_only": "Лише піктограми",
"prefs.imported_file": "Імпортований файл",
"prefs.input": "Введення",
"prefs.interface": "Інтерфейс",
"prefs.interface_language": "Мова інтерфейсу",
"prefs.interface_note": "Як виглядає SolidCraft і де розташовані його частини.",
"prefs.interval_minutes": "{n} хв",
"prefs.labels": "Підписи",
"prefs.layout": "Компонування",
"prefs.layout_note": "«Компактне» прикріплює панелі до країв вікна. Прозорість браузера доступна в режимі «Плаваюче». Сторону Асистента можна змінити на сторінці «Асистент».",
"prefs.left": "Ліворуч",
"prefs.left_click": "Клацання лівою",
"prefs.live_input": "Поточне введення",
"prefs.live_preview": "Попередній перегляд у реальному часі",
"prefs.minutes": "хв",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Миша й трекпад",
"prefs.mouse_heading": "Миша",
"prefs.mouse_mapping": "Призначення кнопок миші",
"prefs.navigation": "Навігація",
"prefs.navigation_note": "Як миша, трекпад і 3D-миша рухають вид.",
"prefs.not_on_gpu": "не на цьому GPU",
"prefs.on": "Увімкнено",
"prefs.opaque": "Непрозорий",
"prefs.orbit_snap": "Утримуйте Shift під час обертання, щоб прив'язатися до осі.",
"prefs.orientation": "Орієнтація",
"prefs.overall_speed": "Загальна швидкість",
"prefs.preview_choices": "Перегляд ваших виборів",
"prefs.quality_default": "типово",
"prefs.quality_fastest": "найшвидше",
"prefs.quality_slowest": "найповільніше",
"prefs.quality_smoother": "плавніше",
"prefs.reports_saved": "Збережено: {n}",
"prefs.right_click": "Клацання правою",
"prefs.saving": "Автозбереження й відновлення",
"prefs.scale_note": "Також Ctrl + / Ctrl − у вікні. Кнопки мають 28 px за 100%.",
"prefs.scale_text": "Масштаб і текст",
"prefs.select": "Вибрати",
"prefs.select_one_item": "Вибрати один елемент",
"prefs.selecting": "Вибір",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift лише додає; Ctrl / Cmd перемикає",
"prefs.shift_add_note": "Увімкнено: Shift додає, Ctrl / Cmd перемикає. Вимкнено (типово): обидва перемикають.",
"prefs.shortcuts": "Комбінації клавіш",
"prefs.sketch_selection": "Ескіз і вибір",
"prefs.sketch_selection_heading": "Ескіз і вибір",
"prefs.sketch_selection_note": "Як ескізи прив'язуються й як клацання вибирають.",
"prefs.sketching": "Ескізування",
"prefs.snap_drawing_note": "Під час малювання або перетягування точки ескізу.",
"prefs.source": "Джерело",
"prefs.speed": "Швидкість",
"prefs.stand_up_note": "Коли файл STEP або IGES відкрито з віссю Z угору, пропонувати поворот на чверть оберту навколо X.",
"prefs.status_done": "Готово",
"prefs.status_error": "Помилка",
"prefs.status_selected": "Вибрано",
"prefs.status_warning": "Попередження",
"prefs.supersampling_brief": "Малює вид у більшому розмірі й зменшує його: дуже чітко, але потребує більше ресурсів.",
"prefs.tab_accent_note": "Акцентний колір відповідає активній вкладці",
"prefs.theme_file": "файл теми",
"prefs.timeline_half_hour": "−30 хв",
"prefs.timeline_now": "зараз",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Додати або прибрати елемент",
"prefs.toolbar": "Панель інструментів",
"prefs.translucent": "Напівпрозорий",
"prefs.under_browser": "Під браузером",
"prefs.under_cursor": "Під курсором",
"prefs.units_precision": "Одиниці й точність",
"prefs.updates": "Оновлення",
"prefs.view": "Поведінка виду",
"prefs.viewport": "Вікно перегляду",
"prefs.wheel": "Колесо",
"prefs.wheel_trackpad": "Колесо й трекпад",
"prefs.workspace": "Робоча область",
"prefs.zoom_toward_cursor": "Масштаб до курсора",
"properties.title": "Властивості: {name}",
"recovery.days_ago": "{count} дн. тому",
"recovery.detail": "{file} · елементів: {features} · автозбережено {when}",
@@ -1361,6 +1655,8 @@
"ui.agent_chat_waiting": "Очікує на вашу відповідь",
"ui.agent_cursor": "Курсор агента",
"ui.agent_cursor_speed": "Швидкість курсора агента",
"ui.agent_off_thread": "Підтримувати чуйність вікна, поки агенти виконують команди",
"ui.agent_off_thread_note": "Повільні команди агента (з панелі ШІ або клієнта MCP) обчислюються паралельно з вікном: ви можете далі обертати й оглядати вид і зупинити команду з індикатора у виді. Вимкнено: кожна виконується в потоці вікна, і вікно чекає на неї.",
"ui.ai_add": "Додати",
"ui.ai_add_agent": "Додати агента…",
"ui.ai_add_agent_title": "Додати агента",
@@ -1389,6 +1685,7 @@
"ui.ai_http_busy": "кінцева точка зайнята або перевищила ліміт; спробуйте пізніше",
"ui.ai_http_key": "кінцева точка відхилила ключ: перевірте його в Налаштування > ШІ",
"ui.ai_http_not_found": "немає такої кінцевої точки чи моделі: перевірте базову URL-адресу й назву моделі",
"ui.ai_http_too_large": "розмова завелика для цієї кінцевої точки; почніть новий чат",
"ui.ai_key": "Ключ API",
"ui.ai_key_hint": "необов'язково; зберігається в системному сховищі ключів",
"ui.ai_key_needs_https": "Ключ надсилається лише через HTTPS або на цей комп'ютер.",
@@ -1592,6 +1889,8 @@
"ui.filter_commands_or_keys": "Фільтрувати команди або клавіші",
"ui.findInTimeline": "Знайти в часовому ланцюжку",
"ui.findInWindow": "Знайти у вікні",
"ui.fine": "Висока",
"ui.finest": "Максимальна",
"ui.finish_sketch": "Завершити ескіз",
"ui.fit": "Вписати",
"ui.fix_unfix": "Закріпити/розкріпити",
@@ -1680,6 +1979,7 @@
"ui.heading_torus": "ТОР",
"ui.heading_utilities": "УТИЛІТИ",
"ui.heading_utility": "УТИЛІТА",
"ui.heading_weldments": "ЗВАРЮВАННЯ",
"ui.help": "Довідка",
"ui.hide": "Приховати",
"ui.hideAxes": "Приховати осі",
@@ -1689,10 +1989,17 @@
"ui.hideProfile": "Приховати профіль",
"ui.history_back": "На один крок назад",
"ui.history_below_browser": "Під браузером",
"ui.history_edge_hint": "двічі клацніть край",
"ui.history_first": "Перейти на початок",
"ui.history_forward": "На один крок уперед",
"ui.history_hide_names": "Приховати назви",
"ui.history_last": "Перейти в кінець",
"ui.history_marker_tip": "Маркер історії: перетягніть, щоб відкотити модель назад; нові елементи додаються тут",
"ui.history_names": "Назви",
"ui.history_play": "Відтворити до кінця",
"ui.history_show_names": "Показати назви",
"ui.history_title": "Історія",
"ui.history_toggle_names": "Показувати або приховувати назви елементів",
"ui.hole": "Отвір",
"ui.hole_tap_type": "Тип різьби отвору",
"ui.hole_type": "Тип отвору",
@@ -1816,6 +2123,7 @@
"ui.project_face_edges_sketch": "Проєктувати ребра граней у нові ескізи",
"ui.properties": "Властивості",
"ui.pull_direction": "Напрямок витягування",
"ui.quality": "Якість",
"ui.quantity": "Кількість",
"ui.quantity_2": "Кількість 2",
"ui.quit": "Вихід",
@@ -1910,6 +2218,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Розділити тіло",
"ui.stand_up_tip": "Після відкриття файлу STEP або IGES з напрямком Z вгору пропонувати поворот на чверть оберту навколо X",
"ui.standard": "Стандартна",
"ui.start": "Початок",
"ui.start_over": "Почати спочатку",
"ui.start_page": "Початкова сторінка",
+7
View File
@@ -971,6 +971,7 @@
"motion.key_here": "Đặt khóa tại đây",
"motion.key_here_hint": "Thêm một khóa tại bước này với giá trị hiện tại của khớp nối",
"motion.no_collisions": "Không có va chạm trong suốt quá trình khảo sát",
"motion.plot_quantity": "Số lượng",
"motion.step_back": "Lùi một bước",
"motion.step_forward": "Tiến một bước",
"motion.to_end": "Đến cuối",
@@ -1389,6 +1390,7 @@
"ui.ai_http_busy": "điểm cuối đang bận hoặc vượt quá giới hạn; vui lòng thử lại sau",
"ui.ai_http_key": "điểm cuối từ chối khóa API: hãy kiểm tra trong Tùy chọn cài đặt > AI",
"ui.ai_http_not_found": "không tìm thấy điểm cuối hoặc mô hình: hãy kiểm tra URL cơ sở và tên mô hình",
"ui.ai_http_too_large": "cuộc trò chuyện quá lớn đối với điểm cuối này; hãy bắt đầu cuộc trò chuyện mới",
"ui.ai_key": "Khóa API",
"ui.ai_key_hint": "tùy chọn; lưu trữ bảo mật trong chuỗi khóa hệ thống",
"ui.ai_key_needs_https": "Khóa chỉ được gửi qua HTTPS, hoặc trực tiếp trên máy tính này.",
@@ -1592,6 +1594,8 @@
"ui.filter_commands_or_keys": "Lọc lệnh hoặc phím tắt",
"ui.findInTimeline": "Tìm trong lịch sử thiết kế",
"ui.findInWindow": "Tìm trong cửa sổ",
"ui.fine": "Mịn",
"ui.finest": "Mịn nhất",
"ui.finish_sketch": "Hoàn tất phác thảo",
"ui.fit": "Vừa vặn (Fit)",
"ui.fix_unfix": "Cố định/Uncố định",
@@ -1680,6 +1684,7 @@
"ui.heading_torus": "HÌNH XUYẾN",
"ui.heading_utilities": "TIỆN ÍCH",
"ui.heading_utility": "TIỆN ÍCH",
"ui.heading_weldments": "KẾT CẤU HÀN",
"ui.help": "Trợ giúp",
"ui.hide": "Ẩn",
"ui.hideAxes": "Ẩn Các trục",
@@ -1816,6 +1821,7 @@
"ui.project_face_edges_sketch": "Chiếu các cạnh bề mặt vào phác thảo mới",
"ui.properties": "Thuộc tính cơ lý",
"ui.pull_direction": "Hướng kéo",
"ui.quality": "Chất lượng",
"ui.quantity": "Số lượng",
"ui.quantity_2": "Số lượng 2",
"ui.quit": "Thoát ứng dụng",
@@ -1910,6 +1916,7 @@
"ui.spline": "Đường cong Spline",
"ui.split_body": "Chia tách khối",
"ui.stand_up_tip": "Sau khi mở tệp STEP hoặc IGES với trục Z hướng lên, đề xuất xoay 90° quanh trục X",
"ui.standard": "Tiêu chuẩn",
"ui.start": "Bắt đầu",
"ui.start_over": "Bắt đầu lại",
"ui.start_page": "Trang bắt đầu",
+7
View File
@@ -970,6 +970,7 @@
"motion.key_here": "在此建立關鍵影格",
"motion.key_here_hint": "在此步驟新增關鍵影格,使用接頭目前的值",
"motion.no_collisions": "整個研究中沒有碰撞",
"motion.plot_quantity": "數量",
"motion.step_back": "上一步",
"motion.step_forward": "下一步",
"motion.to_end": "到結尾",
@@ -1332,6 +1333,7 @@
"ui.ai_http_busy": "端點繁忙或已達上限,請稍後再試",
"ui.ai_http_key": "端點拒絕了金鑰:請在「偏好設定 > AI」中檢查",
"ui.ai_http_not_found": "找不到該端點或模型:請檢查基礎 URL 和模型名稱",
"ui.ai_http_too_large": "對話對此端點來說過大;請開始新的聊天",
"ui.ai_key": "API 金鑰",
"ui.ai_key_hint": "可選;儲存在系統鑰匙圈中",
"ui.ai_key_needs_https": "金鑰只會透過 HTTPS 傳送,或傳送到本機。",
@@ -1534,6 +1536,8 @@
"ui.filter_commands_or_keys": "篩選命令或按鍵",
"ui.findInTimeline": "在時間軸中查詢",
"ui.findInWindow": "在視窗中查詢",
"ui.fine": "精細",
"ui.finest": "最精細",
"ui.finish_sketch": "完成草圖",
"ui.fit": "適合",
"ui.fix_unfix": "固定/取消固定",
@@ -1622,6 +1626,7 @@
"ui.heading_torus": "圓環體",
"ui.heading_utilities": "工具",
"ui.heading_utility": "實用工具",
"ui.heading_weldments": "焊件",
"ui.help": "說明",
"ui.hide": "隱藏",
"ui.hideAxes": "隱藏軸",
@@ -1758,6 +1763,7 @@
"ui.project_face_edges_sketch": "將面的邊投影到新草圖",
"ui.properties": "屬性",
"ui.pull_direction": "拔模方向",
"ui.quality": "品質",
"ui.quantity": "數量",
"ui.quantity_2": "數量 2",
"ui.quit": "結束",
@@ -1852,6 +1858,7 @@
"ui.spline": "雲形線",
"ui.split_body": "分割實體",
"ui.stand_up_tip": "當 Z 朝上的 STEP 或 IGES 檔案開啟後,提示繞 X 軸旋轉四分之一週",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "重新開始",
"ui.start_page": "起始頁面",
+147 -3
View File
@@ -143,6 +143,7 @@
"Cube": "立方体",
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
"Current perspective view": "当前透视视图",
"Current plane": "当前平面",
"Curv. Comb": "曲率梳",
"Curvature (G2)": "曲率 (G2)",
"Curvature change (G3)": "曲率变化 (G3)",
@@ -172,7 +173,12 @@
"Don't ask again": "不再询问",
"Double-click to rename": "双击重命名",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作",
"Drag to move the boss across the model. It snaps to sketch points on the face.": "拖动以在模型上移动凸台。它会吸附到面上的草图点。",
"Drag to move the hole; click to remove it": "拖动以移动孔,单击以删除",
"Drag to move the rest across the model. It snaps to sketch points on the face.": "拖动以在模型上移动垫块。它会吸附到面上的草图点。",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "拖动以在模型上移动卡扣。它会吸附到面上的草图点。",
"Drag to point the catch in another direction.": "拖动以将卡钩指向另一个方向。",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "拖动以 5° 为步长旋转垫块。按住 Alt 或 Ctrl 可自由旋转。",
"Drive Joint": "驱动接头",
"Duration": "时长",
"Edges": "边",
@@ -239,7 +245,9 @@
"Gap": "间隙",
"Grid": "网格",
"HISTORY": "历史",
"Halo under sketch curves": "在草图曲线下方显示光晕",
"Height": "高度",
"Height From": "高度基准",
"Hidden edges": "隐藏线",
"History layout": "历史布局",
"History names": "历史名称",
@@ -256,6 +264,7 @@
"In place (surfaces)": "原位(曲面)",
"In plane": "在平面内",
"Included": "夹角",
"Inner Sharp": "内侧虚拟尖点",
"Inside": "内侧",
"Instant": "即时",
"Interface font": "界面字体",
@@ -302,6 +311,7 @@
"Mass Center": "质心",
"Matched restrictions check the final values.": "满足条件的限制规则检查最终值。",
"Material": "材质",
"Material Outside": "材料在外",
"Material copied. Existing body assignments are unchanged.": "材质已复制。现有实体指定保持不变。",
"Max": "最大",
"Maximum": "最大",
@@ -314,7 +324,6 @@
"Mitre": "斜接",
"Model Parameters": "模型参数",
"Modeled": "已建模",
"More themes": "更多主题",
"More toolbar panels": "更多工具栏面板",
"Motion": "运动",
"Mouse gestures": "鼠标手势",
@@ -349,8 +358,10 @@
"On your computer": "在此计算机上",
"Opacity": "不透明度",
"Open GitHub draft": "打开 GitHub 草稿",
"Open bodies (no volume)": "未闭合实体(无体积)",
"Open library...": "打开库...",
"Open local libraries in the desktop app. You can export this design's library here.": "请在桌面应用中打开本地库。您可以在此导出当前设计的库。",
"Open surface — no volume": "未闭合曲面 — 无体积",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
"Opens your browser and copies the report text for you to paste into GitHub.": "打开浏览器,并复制报告文本以便粘贴到GitHub。",
"Opens your browser. Review and submit your report on GitHub.": "打开浏览器。请在GitHub中检查并提交报告。",
@@ -359,6 +370,7 @@
"Other side": "另一侧",
"Other surface (mutual trim)": "另一曲面(相互修剪)",
"Other way": "反方向",
"Outer Sharp": "外侧虚拟尖点",
"Outer radius": "外圆角半径",
"Outside": "外侧",
"Overall length": "总长",
@@ -388,11 +400,10 @@
"Preview": "预览",
"Preview Row": "预览行",
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
"Previous themes": "上一组主题",
"Profile / Edges": "轮廓 / 边",
"Profiles or curves": "轮廓或曲线",
"Profiles/Curves": "轮廓/曲线",
"Quantity": "物理量",
"Quantity": "数量",
"RECENT": "最近使用",
"REFERENCE": "参考",
"RULED SURFACE": "直纹曲面",
@@ -526,9 +537,11 @@
"Surface, or faces to turn over": "曲面,或要翻转的面",
"Swap Ends": "交换端点",
"TRIM": "修剪",
"Tangent": "相切",
"Tangent (G1)": "相切 (G1)",
"Tangent Chain": "相切链",
"Tangent chain": "相切链",
"Tangent to Edge": "与边相切",
"Tap drill diameter": "攻丝底孔直径",
"Target": "目标",
"Target Body": "目标实体",
@@ -684,6 +697,7 @@
"cmd.component_capture_position": "捕获位置",
"cmd.component_copies": "查找相同副本",
"cmd.component_create": "新建组件",
"cmd.component_cycle_color": "循环切换组件颜色",
"cmd.component_delete": "删除组件",
"cmd.component_extract": "提取到文件",
"cmd.component_from_bodies": "由实体创建组件",
@@ -745,6 +759,7 @@
"cmd.document_metadata": "设计属性",
"cmd.document_metadata_remove": "删除设计属性",
"cmd.document_metadata_set": "更改设计属性",
"cmd.document_revision": "设计版本号",
"cmd.document_units": "更改设计单位",
"cmd.edit_checkpoint": "检查点",
"cmd.edit_restore_checkpoint": "恢复检查点",
@@ -768,6 +783,8 @@
"cmd.inspect_accessibility": "可达性分析",
"cmd.inspect_center_of_mass": "质心",
"cmd.inspect_clearance": "间隙",
"cmd.inspect_colored_components": "启用组件颜色",
"cmd.inspect_colored_components_disable": "禁用组件颜色",
"cmd.inspect_compare": "比较实体",
"cmd.inspect_continuity": "连续性分析",
"cmd.inspect_curvature_comb": "曲率梳分析",
@@ -908,6 +925,7 @@
"cmd.sheet_refold": "重新折弯",
"cmd.sheet_rules": "列出钣金规则",
"cmd.sheet_unfold": "展开",
"cmd.sketch_arc3d": "三维圆弧",
"cmd.sketch_arc_center_point": "中心点圆弧",
"cmd.sketch_arc_tangent": "相切圆弧",
"cmd.sketch_arc_three_point": "三点圆弧",
@@ -1030,17 +1048,25 @@
"cmd.view_back": "后视图",
"cmd.view_bottom": "仰视图",
"cmd.view_delete": "删除命名视图",
"cmd.view_fit": "适合视图",
"cmd.view_front": "前视图",
"cmd.view_graphics_preset": "图形预设",
"cmd.view_grid": "网格",
"cmd.view_ground_shadow": "地面阴影",
"cmd.view_home": "主视图",
"cmd.view_isometric": "等轴测视图",
"cmd.view_left": "左视图",
"cmd.view_list": "列出命名视图",
"cmd.view_navigate": "导航视图",
"cmd.view_navigation_bar": "导航栏",
"cmd.view_normal_to": "垂直于所选面",
"cmd.view_rename": "重命名命名视图",
"cmd.view_right": "右视图",
"cmd.view_save": "新建命名视图",
"cmd.view_spin": "连续环绕",
"cmd.view_tool": "导航工具",
"cmd.view_top": "俯视图",
"cmd.view_visual_style": "视觉样式",
"color.copy": "单击复制颜色值",
"color.edit": "单击编辑颜色",
"color.gamma": "以 0–255 伽马空间显示颜色值",
@@ -1081,6 +1107,7 @@
"crash.report": "报告…",
"crash.unclean_explanation": "在后续启动时检测到;未记录确切的退出时间。系统关机或强制停止也可能导致会话未正常结束。",
"delete.and_more": "…以及另外 {count} 项",
"diagnostic.empty_boolean_component": "组件 '{component}' 中没有可供切除或求交的实体;工具可能与组件 '{other}' 中的 '{body}' 重叠 — 请激活该组件或选择该实体作为目标",
"dialog.bodies_count": "{count} 个实体",
"dialog.clearance_invalid": "间隙必须是 0 或更大的长度",
"dialog.compute": "计算",
@@ -1124,9 +1151,11 @@
"dxf.progress.write": "正在写入 DXF (5/5)",
"dxf.tolerance": "曲线公差",
"dxf.tolerance_help": "仅导出可见实体和显示的草图。曲线使用直线段近似。公差越小,曲线越精细,但需要更多时间和内存。",
"file.open.drawing_filter": "设计、CAD 文件、网格和图纸",
"file_load.built": "已构建:{built}/{total} 个特征。",
"file_load.converting": "正在转换实体 {done}/{total}",
"file_load.inserting": "正在插入几何体",
"file_load.link_item": "{name} ({done}/{total})",
"file_load.opening": "正在打开设计",
"file_load.preparing_thumbnail": "正在准备缩略图...",
"file_load.preparing_view": "正在准备视图...",
@@ -1136,6 +1165,7 @@
"file_load.rebuilding": "正在重建特征 {done}/{total}",
"file_load.recovering": "正在恢复设计",
"file_load.stopping": "正在等待上一次操作停止...",
"file_load.updating_links": "正在更新链接",
"file_load.waiting": "正在等待文件操作",
"fits the face, e.g. M8": "适配该面,例如 M8",
"from the profile": "取自型材",
@@ -1163,6 +1193,26 @@
"help.solid.shell": "将实体掏空,留下指定厚度的壁,还可以打开所选的面。用于盒子、盖子等薄壁零件。",
"help.solid.sweep": "沿路径拖动草图轮廓生成实体。用于管道、导轨、手柄等截面恒定且沿曲线延伸的零件。",
"help.third_party": "第三方许可证",
"hole_presets.confirm_delete": "确认删除",
"hole_presets.delete": "删除",
"hole_presets.delete_prompt": "删除此个人预设?现有孔不会改变。",
"hole_presets.full": "个人库中已有64个预设。",
"hole_presets.invalid_name": "请输入不含换行符或控制字符的简短名称。",
"hole_presets.invalid_parameters": "请使用长度适当的有效表达式填写孔参数。",
"hole_presets.missing": "所选预设已不可用。",
"hole_presets.name": "预设名称",
"hole_presets.name_exists": "同名预设已存在,请使用其他名称。",
"hole_presets.name_hint": "唯一名称",
"hole_presets.overwrite": "保存对所选预设的修改",
"hole_presets.preset": "预设",
"hole_presets.recall": "调用",
"hole_presets.rename": "重命名",
"hole_presets.save": "另存为新预设",
"hole_presets.scope": "保存在您的配置中,适用于所有项目。",
"hole_presets.select": "选择预设",
"hole_presets.title": "个人预设",
"hole_presets.unsupported_options": "此预设使用了当前版本无法调用的螺纹选项。",
"hole_presets.unsupported_overwrite": "此预设包含无法安全覆盖的选项。",
"home.back": "返回 {name}",
"home.crashes": "崩溃报告",
"home.design_workspace": "设计",
@@ -1238,6 +1288,8 @@
"joint.from_bodies_hint": "每个所选实体移入各自的新组件,位置保持不变。",
"joint.one_body": "两次选择都在 {body} 上。请在另一个零件上选择第二个。",
"joint.one_component": "这些零件是同一组件的实体:{bodies}。接头使一个组件相对另一个组件运动,因此每个零件都需要自己的组件。",
"joint.rigid_group_one_component": "这 {count} 个实体都位于同一组件中(或直接位于设计中):请先使用“{command}”。",
"joint.rigid_group_select_components": "请选择两个或更多组件。",
"keymap.command_id": "命令 ID",
"keymap.empty": "没有匹配的命令",
"keymap.use_anyway": "仍在此处使用 {key}",
@@ -1309,6 +1361,7 @@
"motion.key_here": "在此添加关键帧",
"motion.key_here_hint": "在此步骤添加关键帧,使用关节的当前值",
"motion.no_collisions": "整个研究中没有碰撞",
"motion.plot_quantity": "物理量",
"motion.step_back": "上一步",
"motion.step_forward": "下一步",
"motion.to_end": "到结尾",
@@ -1321,6 +1374,7 @@
"nav.fusion.help": "中键拖动平移,Shift+中键拖动旋转,右键拖动旋转",
"nav.inventor.help": "中键拖动平移,Shift+中键拖动旋转,Ctrl+中键拖动缩放,右键拖动旋转",
"nav.openscad.help": "在空白处左键拖动旋转,Shift+左键拖动框选,拖动草图几何可编辑;右键或中键拖动平移,Shift+右键或 Shift+中键拖动缩放",
"nav.perspective.sketch": "编辑草图时使用正交视图。完成草图后才能切换透视。",
"nav.solidworks.help": "中键拖动旋转,Ctrl+中键拖动平移,Shift+中键拖动缩放,右键拖动旋转",
"nav.tinkercad.help": "右键拖动旋转,中键或 Shift+右键拖动平移",
"nav.touchpad.help": "按住 Shift 并移动指针可平移;按住 Alt/Option 并移动可旋转。无需按鼠标按钮。垂直滚动可缩放。",
@@ -1330,6 +1384,10 @@
"nav.touchpad.scroll": "触控板使用垂直滚动进行缩放。双指平移设置将保留供其他预设使用。",
"nav.touchpad.zoom": "缩放(垂直滚动或双指捏合)",
"needs two joints": "需要两个接头",
"network.allow_once": "仅允许一次",
"network.always_allow": "始终允许此服务器",
"network.prompt_detail": "此设计链接到网络位置 {host} 上的文件。打开它会连接到该计算机,并可能向其发送你的 Windows 登录信息。只允许你信任的服务器。",
"network.prompt_title": "网络位置",
"new configuration": "新配置",
"new parameter": "新参数",
"no hole": "无孔",
@@ -1358,6 +1416,7 @@
"pick the path": "选择路径",
"prefs.add_item": "添加项目",
"prefs.alt_frees_manipulators": "Alt / Option 也可自由拖动操纵器箭头",
"prefs.anti_aliasing_amd_dx12": "由于启动检查在启用多重采样时未绘制任何内容,而在1×时通过了检查,抗锯齿已设为1×。您可以在上方选择其他设置;重启后若检查通过,该设置将生效。",
"prefs.anti_aliasing_note": "平滑锯齿边缘。",
"prefs.applies_on_restart": "重启后生效",
"prefs.assistant": "助手",
@@ -1401,6 +1460,10 @@
"prefs.gesture_pan": "平移",
"prefs.gesture_zoom": "缩放",
"prefs.graphics": "图形",
"prefs.graphics_card": "显卡",
"prefs.graphics_card_auto": "自动",
"prefs.graphics_card_missing": "已保存的显卡不可用;将使用自动选择。",
"prefs.graphics_card_restart": "重启 SolidCraft 后生效。WGPU_ADAPTER_NAME 优先于此选择。",
"prefs.graphics_note": "在速度与平滑度之间取舍。每个选项均以示例展示效果。",
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
"prefs.history": "历史",
@@ -1426,12 +1489,18 @@
"prefs.mouse_mapping": "鼠标映射",
"prefs.navigation": "导航",
"prefs.navigation_note": "鼠标、触控板和3D鼠标如何移动视图。",
"prefs.network_host_remove": "删除",
"prefs.network_locations": "允许设计中的网络位置",
"prefs.network_locations_note": "关闭:设计中位于其他计算机 (\\\\server\\share) 上的链接文件,只会从此处列出的服务器打开,或在询问时你允许的服务器打开。",
"prefs.network_locations_section": "网络位置",
"prefs.not_on_gpu": "此 GPU 不支持",
"prefs.on": "开启",
"prefs.opaque": "不透明",
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
"prefs.orientation": "方向",
"prefs.overall_speed": "整体速度",
"prefs.placement": "放置",
"prefs.prefer_free_sketch_points": "草图点重合时优先选择可移动的点",
"prefs.preview_choices": "所选设置预览",
"prefs.quality_default": "默认",
"prefs.quality_fastest": "最快",
@@ -1449,6 +1518,11 @@
"prefs.shift_add_ctrl_toggle": "Shift 仅添加;Ctrl / Cmd 切换选择",
"prefs.shift_add_note": "开启:Shift 添加,Ctrl / Cmd 切换选择。关闭(默认):两者均切换选择。",
"prefs.shortcuts": "快捷键",
"prefs.sketch_exit_view": "草图结束时的视图",
"prefs.sketch_exit_view.keep": "保持当前视图",
"prefs.sketch_exit_view.previous": "返回之前的视图",
"prefs.sketch_exit_view.unless_rotated": "未旋转时返回",
"prefs.sketch_exit_view_note": "“完成草图”、取消或连按两次 Esc 后视图的去向。",
"prefs.sketch_selection": "草图与选择",
"prefs.sketch_selection_heading": "草图和选择",
"prefs.sketch_selection_note": "草图如何捕捉,以及单击如何选择。",
@@ -1475,6 +1549,8 @@
"prefs.units_precision": "单位与精度",
"prefs.updates": "更新",
"prefs.view": "视图行为",
"prefs.view_confirm_corner": "在视图角落显示确定和取消",
"prefs.view_confirm_corner_note": "打开对话框或草图时,视图右上角的勾号和叉号与对话框的确定和取消作用相同,或完成和取消草图。视图导航器会下移到它们下方。",
"prefs.viewport": "视口",
"prefs.wheel": "滚轮",
"prefs.wheel_trackpad": "滚轮与触控板",
@@ -2138,6 +2214,11 @@
"sketch_panel.author": "创建时的放置:{path}",
"sketch_panel.dimension_hint": "输入数值或表达式,然后按“确定”。",
"sketch_panel.move_hint": "输入距离和角度,然后按“确定”。",
"sketch_panel.plane_angle": "{plane},旋转 {angle}",
"sketch_panel.plane_fixed": "固定平面",
"sketch_panel.plane_flipped": "{plane}(从另一侧)",
"sketch_panel.plane_offset": "{plane},偏移 {distance}",
"sketch_panel.plane_origin": "{plane} 平面",
"sketch_panel.redefine_title": "{command}:{name}",
"sketch_panel.sheet_attachment": "附着到钣金实体的平面上;钣金变化时会跟随该面板。",
"sketch_panel.text_hint": "编辑文字及其位置,然后按“确定”。",
@@ -2157,12 +2238,18 @@
"the parts library is not available": "零件库不可用",
"thread.automatic_size": "自动尺寸",
"thread.designation": "螺纹标记",
"thread.direction": "螺纹方向",
"thread.iso_metric": "ISO 公制",
"thread.left_hand": "左旋",
"thread.right_hand": "右旋",
"thread.select_size": "选择尺寸",
"timeline.delete_after_marker_one": "删除历史标记之后的 1 个特征",
"timeline.delete_after_marker_other": "删除历史标记之后的 {count} 个特征",
"timeline.delete_folders_tip": "在历史记录中保留其中的特征和嵌套文件夹",
"timeline.delete_many": "删除所选的 {count} 个特征",
"timeline.feature_error": "错误:{message}",
"timeline.feature_hint": "双击编辑 · 拖动以重新排序 · 右键查看更多操作",
"timeline.feature_warning": "警告:{message}",
"timeline.group": "新建特征文件夹",
"timeline.move_many": "移动时间线项目",
"timeline.move_to_group": "将特征移至文件夹",
@@ -2312,6 +2399,7 @@
"ui.agent_chat_changes": "{n} 项更改,一步撤销",
"ui.agent_chat_changes_n": "{n} 项更改",
"ui.agent_chat_changes_of": "更改 {n}/{limit}",
"ui.agent_chat_choose_model": "选择模型",
"ui.agent_chat_close": "关闭",
"ui.agent_chat_command": "智能体命令",
"ui.agent_chat_connect": "连接",
@@ -2358,10 +2446,12 @@
"ui.agent_chat_key_in_env": "已设置密钥",
"ui.agent_chat_left": "左侧",
"ui.agent_chat_limit": "智能体已达到每条消息 {limit} 项更改的上限。再发送一条消息可让它继续。",
"ui.agent_chat_login_found": "已找到登录信息",
"ui.agent_chat_minimize": "最小化为细条",
"ui.agent_chat_mode_all": "全部",
"ui.agent_chat_mode_ask": "询问",
"ui.agent_chat_mode_edits": "编辑",
"ui.agent_chat_model_no_tools": "{model} 无法使用 SolidCraft 的工具,只能聊天。请选择支持工具的模型。",
"ui.agent_chat_model_tip": "智能体使用的模型。你的选择会为此智能体记住。",
"ui.agent_chat_new": "新对话",
"ui.agent_chat_new_confirm": "开始新对话?当前对话将被清除。",
@@ -2370,6 +2460,7 @@
"ui.agent_chat_no_control": "智能体聊天的端点未能启动,因此智能体无法访问设计。",
"ui.agent_chat_no_images": "该智能体不接收图片",
"ui.agent_chat_no_picture": "剪贴板上没有图片",
"ui.agent_chat_no_tool_call": "模型没有使用 SolidCraft 的工具,因此设计没有更改。请选择支持工具调用的模型;参见智能体聊天指南中的“本地模型”。",
"ui.agent_chat_none_found": "未找到编程智能体。请安装以下之一,然后点击刷新:",
"ui.agent_chat_none_found_short": "未找到智能体",
"ui.agent_chat_not_installed": "未安装",
@@ -2387,6 +2478,9 @@
"ui.agent_chat_previous_picture": "上一张图片",
"ui.agent_chat_privacy": "除了你以自己的账户运行的智能体之外,不会向任何地方发送任何内容。",
"ui.agent_chat_privacy_note": "SolidCraft 不向任何地方发送任何内容:你运行的智能体以你自己的账户与它自己的服务通信。SolidCraft 从不查看或保存任何密钥或密码。",
"ui.agent_chat_provider_models": "{count} 个模型",
"ui.agent_chat_provider_other": "其他",
"ui.agent_chat_provider_tip": "旁边的列表显示此提供商的模型。",
"ui.agent_chat_queue": "加入队列",
"ui.agent_chat_queue_full": "等待中的消息太多:请删除一条,或等待智能体完成。",
"ui.agent_chat_queued": "已排队",
@@ -2455,6 +2549,7 @@
"ui.ai_http_busy": "端点繁忙或已达上限,请稍后再试",
"ui.ai_http_key": "端点拒绝了密钥:请在“首选项 > AI”中检查",
"ui.ai_http_not_found": "找不到该端点或模型:请检查基础 URL 和模型名称",
"ui.ai_http_too_large": "对话对此端点来说过大,请开始新的聊天",
"ui.ai_key": "API 密钥",
"ui.ai_key_hint": "可选;保存在系统钥匙串中",
"ui.ai_key_needs_https": "密钥只会通过 HTTPS 发送,或发送到本机。",
@@ -2501,9 +2596,27 @@
"ui.ai_test_no_model": "已连接:{count} 个模型,但没有 {model}",
"ui.ai_test_ok": "已连接:{count} 个模型",
"ui.ai_testing": "正在测试…",
"ui.ai_tool_arguments": "参数",
"ui.ai_tool_details": "详细信息",
"ui.ai_tool_done": "完成",
"ui.ai_tool_failed": "失败",
"ui.ai_tool_result": "结果",
"ui.ai_tool_running": "运行中",
"ui.ai_tool_sum_bodies_one": "{count} 个实体",
"ui.ai_tool_sum_bodies_other": "{count} 个实体",
"ui.ai_tool_sum_by": "距离 {value}",
"ui.ai_tool_sum_edges_one": "{count} 条边",
"ui.ai_tool_sum_edges_other": "{count} 条边",
"ui.ai_tool_sum_faces_one": "{count} 个面",
"ui.ai_tool_sum_faces_other": "{count} 个面",
"ui.ai_tool_sum_features_one": "{count} 个特征",
"ui.ai_tool_sum_features_other": "{count} 个特征",
"ui.ai_tool_sum_measured": "已测量 {what}",
"ui.ai_tool_sum_more": "{steps},另有 {count} 项",
"ui.ai_tool_sum_radius": "R {value}",
"ui.ai_tool_sum_saved": "已保存 {file}",
"ui.ai_tool_sum_screenshot": "截图",
"ui.ai_tool_sum_then": "{first},然后 {next}",
"ui.ai_tool_unfold": "点击显示返回内容",
"ui.ai_tool_waiting": "等待中",
"ui.ai_toolbar_button": "在工具栏显示按钮",
@@ -2594,6 +2707,8 @@
"ui.copy": "复制",
"ui.corner": "角",
"ui.corners": "角点",
"ui.cosmetic_thread_markings": "装饰螺纹标记",
"ui.cosmetic_thread_markings_help": "在带螺纹的圆柱面上显示示意标记。仅改变显示,不影响设计或导出。",
"ui.counterbore": "沉头孔",
"ui.countersink": "锥形沉孔",
"ui.create": "创建",
@@ -2670,6 +2785,8 @@
"ui.filter_commands_or_keys": "筛选命令或按键",
"ui.findInTimeline": "在时间线中查找",
"ui.findInWindow": "在窗口中查找",
"ui.fine": "精细",
"ui.finest": "最精细",
"ui.finish_sketch": "完成草图",
"ui.fit": "适合",
"ui.fix_unfix": "固定/取消固定",
@@ -2759,6 +2876,7 @@
"ui.heading_torus": "圆环体",
"ui.heading_utilities": "工具",
"ui.heading_utility": "实用工具",
"ui.heading_weldments": "焊件",
"ui.help": "帮助",
"ui.hide": "隐藏",
"ui.hideAxes": "隐藏轴",
@@ -2845,6 +2963,11 @@
"ui.move": "移动",
"ui.multiple": "多个",
"ui.named_views": "命名视图",
"ui.nav_bridge": "使用本机上的设备程序导航",
"ui.nav_bridge_connected": "已连接",
"ui.nav_bridge_port": "端口",
"ui.nav_bridge_tip": "3D 输入设备自带的程序(例如自制 3D 鼠标的驱动程序)通过本地连接移动视图,鼠标指针仍由你使用。SolidCraft 只在本机的此端口上连接它。",
"ui.nav_bridge_waiting": "正在等待设备程序",
"ui.navigation": "导航",
"ui.needs_design": "{command}: 请先打开一个设计",
"ui.newSketchGroup": "新建草图组",
@@ -2863,6 +2986,7 @@
"ui.one_side": "单侧",
"ui.open": "打开",
"ui.open_ends_tip": "未接触任何曲线",
"ui.open_linked_design": "打开链接设计",
"ui.open_preferences": "打开首选项",
"ui.open_recent": "打开最近",
"ui.operation": "操作",
@@ -2882,18 +3006,27 @@
"ui.paste": "粘贴",
"ui.path": "路径",
"ui.pattern_on_path": "路径阵列",
"ui.perforate_margin_default": "与直径相同",
"ui.perforate_pitch": "孔距",
"ui.perforate_square": "正方形排列",
"ui.perforate_staggered": "交错排列(60°)",
"ui.perforate_title": "穿孔",
"ui.perpendicular": "垂直",
"ui.perspective_view": "透视图",
"ui.pinToShortcuts": "固定到快捷方式",
"ui.pinToToolbar": "固定到工具栏",
"ui.pipe": "管道",
"ui.placement": "放置",
"ui.placement_handles": "放置手柄",
"ui.placement_handles_tip": "垫块、凸台或卡扣预览上的手柄:拖动标记在模型上移动,用转环旋转",
"ui.plane": "平面",
"ui.plane_at_angle": "角度平面",
"ui.plugins": "插件",
"ui.plugins_desktop": "插件仅在桌面版中可用。",
"ui.plugins_empty": "尚未安装插件。",
"ui.plugins_none": "没有符合搜索的插件。",
"ui.plugins_note": "每个插件是 {path} 里的一个文件夹,其中有 plugin.json。SolidCraft 只读取该文件,不运行文件夹里的代码。关闭插件仍会保留安装。",
"ui.plugins_search": "搜索插件",
"ui.plugins_skipped": "有 {count} 个文件夹无法读取。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2909,6 +3042,7 @@
"ui.project_face_edges_sketch": "将面的边投影到新草图",
"ui.properties": "属性",
"ui.pull_direction": "拔模方向",
"ui.quality": "质量",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "退出",
@@ -3015,6 +3149,7 @@
"ui.spline": "样条曲线",
"ui.split_body": "分割实体",
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
"ui.standard": "标准",
"ui.start": "开始",
"ui.start_over": "重新开始",
"ui.start_page": "起始页",
@@ -3054,6 +3189,7 @@
"ui.total_angle": "总角度",
"ui.trackpad_pan_tip": "双指滚动平移;Fusion 预设下,Shift + 双指滚动旋转视图。关闭时滚动缩放。鼠标滚轮和双指捏合缩放,拇指滚轮横向平移。",
"ui.trim": "修剪",
"ui.turn_up_axis": "旋转向上轴",
"ui.twist": "扭转",
"ui.two_finger_scroll_pans": "双指滚动平移",
"ui.two_sides": "双侧",
@@ -3071,6 +3207,7 @@
"ui.updateToCurrentView": "更新为当前视图",
"ui.vertical_constraint": "垂直约束",
"ui.view_centre": "视图中心",
"ui.viewport_theme_follow": "跟随主题",
"ui.visual_shaded_hidden_edges": "带隐藏边的着色",
"ui.visual_shaded_with_edges": "带边着色",
"ui.visual_style": "视觉样式",
@@ -3124,6 +3261,7 @@
"update.prefs_flatpak": "此副本({version})由 Flatpak 更新:运行 {command} 或使用软件中心。SolidCraft 不会自行检查。",
"update.prefs_label": "更新",
"update.prefs_note": "当前为 SolidCraft {version}({kind})。检查时只向 GitHub 查询最新版本,不发送其他任何数据。",
"update.prefs_web": "重新加载页面以获取最新的网页应用。如果网络不可用或更新无法及时下载完成,则打开已保存的版本;正在进行的下载会继续,供下次重新加载时使用。",
"update.release_page": "打开发布页面",
"update.restart": "立即重启",
"update.restart_later": "稍后",
@@ -3134,6 +3272,12 @@
"update.up_to_date": "SolidCraft 已是最新版本。",
"update.update_now": "立即更新",
"update.you_have": "当前版本为 {version}。",
"view.auto_ortho_faces": "旋转时透视,正对表面时正交",
"web.files.insert_drawings_hint": "将 CAD 文件、网格、DXF 或 SVG 插入当前设计",
"web.files.insert_stored_drawings_hint": "插入当前设计(CAD 文件、网格、DXF、SVG)",
"web.open_full": "在 SolidCraft 中打开",
"web.open_link": "打开链接的设计?",
"web.open_link_confirm": "有未保存的更改。要在新标签页中打开链接的设计吗?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 选择 · Enter / Tab 补全 · Esc 关闭",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ 选择 · Enter 按原样应用 · Esc 关闭"
}
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
{
"window": [540, 360],
"title_bar": 32,
"icon_size": 148,
"text_size": 12,
"app": [132, 176],
"applications": [408, 176]
}
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Draw the SolidCraft disk image window background (background.tiff).
A blueprint sheet: deep blue with a drafting grid, a double border with ticks, a circle round each
icon, dash-dot centre lines, a leader arrow from the app to Applications and a small title block.
Every line stops short of text. The window geometry comes from layout.json, shared with
settings.py, so the art and the icons agree.
Authoring tool only (assets/AGENTS.md): run it after editing, commit the TIFF. Packaging never
runs it. Needs Python 3.11+, Pillow and macOS `tiffutil`:
python3 -m venv target/dmg-art && target/dmg-art/bin/pip install pillow
target/dmg-art/bin/python assets/platforms/macos/dmg/make_background.py [--preview DIR]
All coordinates are points; the TIFF holds a 1x and a 144 dpi 2x representation.
"""
import argparse
import json
import subprocess
import tempfile
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
HERE = Path(__file__).resolve().parent
FONTS = HERE.parents[2] / "fonts"
LAYOUT = json.loads((HERE / "layout.json").read_text())
W, H = LAYOUT["window"]
ICON = LAYOUT["icon_size"]
AX, CY = LAYOUT["app"]
BX, _ = LAYOUT["applications"]
SS = 6 # drawing scale: 3x the 2x output, reduced afterwards
PAPER_TOP, PAPER_BOTTOM = (28, 84, 158), (18, 58, 118)
LINE = (226, 238, 252)
PALE = (190, 215, 245)
# Finder draws the icon names itself, black in Light and white in Dark appearance, and the art
# cannot change that: each name sits on a mid-tone plate that both colours read on.
PLATE = (92, 132, 196)
PLATE_W, PLATE_H, PLATE_GAP = 116, 22, 8
def px(v):
return int(round(v * SS))
def sans(size, weight):
# IBMPlexSans-Variable axes: wght, wdth
f = ImageFont.truetype(str(FONTS / "ibm-plex-sans/IBMPlexSans-Variable.ttf"), px(size))
f.set_variation_by_axes([weight, 100])
return f
def mono(size):
return ImageFont.truetype(str(FONTS / "ibm-plex-mono/IBMPlexMono-Regular.ttf"), px(size))
def text(d, xy, s, f, fill, anchor="la", tracking=0.0):
"""Draw text with letter spacing (points); anchor is the usual Pillow one for the whole run."""
x, y = xy
if tracking == 0:
d.text((px(x), px(y)), s, font=f, fill=fill, anchor=anchor)
return
widths = [f.getlength(c) / SS for c in s]
total = sum(widths) + tracking * (len(s) - 1)
if anchor[0] == "m":
x -= total / 2
elif anchor[0] == "r":
x -= total
for c, w in zip(s, widths):
d.text((px(x), px(y)), c, font=f, fill=fill, anchor="l" + anchor[1])
x += w + tracking
def dashdot(d, p0, p1, fill, width=1.0, pattern=(14, 3, 2, 3)):
"""A centre line: long dash, gap, short dash, gap. Horizontal or vertical."""
(x0, y0), (x1, y1) = p0, p1
horiz = y0 == y1
a, b = (x0, x1) if horiz else (y0, y1)
pos, k = a, 0
while pos < b:
seg = min(pattern[k % 4], b - pos)
if k % 2 == 0:
q, r = ((pos, y0), (pos + seg, y0)) if horiz else ((x0, pos), (x0, pos + seg))
d.line([(px(q[0]), px(q[1])), (px(r[0]), px(r[1]))], fill=fill, width=px(width))
pos += seg
k += 1
def draw_sheet():
img = Image.new("RGB", (px(W), px(H)))
d = ImageDraw.Draw(img)
for y in range(px(H)):
t = y / px(H)
d.line([(0, y), (px(W), y)], fill=tuple(round(a + (b - a) * t) for a, b in zip(PAPER_TOP, PAPER_BOTTOM)))
# Grid on its own layer so a soft radial mask can fade it towards the edges.
grid = Image.new("RGBA", img.size, (0, 0, 0, 0))
g = ImageDraw.Draw(grid)
for i in range(0, W + 1, 12):
g.line([(px(i), 0), (px(i), px(H))], fill=(*PALE, 95 if i % 60 == 0 else 40), width=px(0.6))
for j in range(0, H + 1, 12):
g.line([(0, px(j)), (px(W), px(j))], fill=(*PALE, 95 if j % 60 == 0 else 40), width=px(0.6))
mask = Image.radial_gradient("L").resize(img.size).point(lambda v: max(0, 255 - int(v * 0.7)))
grid.putalpha(Image.composite(grid.getchannel("A"), Image.new("L", img.size, 0), mask))
img = Image.alpha_composite(img.convert("RGBA"), grid)
d = ImageDraw.Draw(img)
# Double border with a tick every 60 pt.
d.rectangle([px(10), px(10), px(W - 10), px(H - 10)], outline=(*LINE, 255), width=px(1.8))
d.rectangle([px(15), px(15), px(W - 15), px(H - 15)], outline=(*LINE, 110), width=px(0.6))
for t in range(60, W, 60):
d.line([(px(t), px(10)), (px(t), px(15))], fill=(*LINE, 255), width=px(0.8))
d.line([(px(t), px(H - 15)), (px(t), px(H - 10))], fill=(*LINE, 255), width=px(0.8))
# Title, top left. The left icon's centre line sits to its right and starts below it.
text(d, (30, 24), "SolidCraft", sans(20, 600), (*LINE, 255))
text(d, (31, 49), "PARAMETRIC 3D CAD", mono(7), (*PALE, 255), tracking=0.9)
# A circle round each icon, with centre lines that stop short of circles, plates and text.
r = ICON / 2 + 6
for cx in (AX, BX):
d.ellipse([px(cx - r), px(CY - r), px(cx + r), px(CY + r)], outline=(*LINE, 170), width=px(1.1))
cl = (*LINE, 120)
dashdot(d, (30, CY), (AX - r - 10, CY), cl)
dashdot(d, (BX + r + 10, CY), (W - 30, CY), cl)
for cx in (AX, BX):
dashdot(d, (cx, 66), (cx, CY - r - 6), cl)
dashdot(d, (cx, CY + r + 34), (cx, H - 42), cl)
# Leader from the app to Applications: origin dot, line, arrowhead, a label above.
x0, x1, y = AX + r + 8, BX - r - 8, CY + 7
d.line([(px(x0 + 4), px(y)), (px(x1 - 10), px(y))], fill=(*LINE, 255), width=px(1.8))
d.ellipse([px(x0 - 3), px(y - 3), px(x0 + 3), px(y + 3)], fill=(*LINE, 255))
d.polygon([(px(x1), px(y)), (px(x1 - 11), px(y - 4.4)), (px(x1 - 11), px(y + 4.4))], fill=(*LINE, 255))
text(d, ((x0 + x1) / 2, CY - 6), "DRAG TO INSTALL", mono(8.5), (*LINE, 255), "ms", tracking=1.4)
# Title block, bottom right: two cells.
bx1, by1 = W - 16, H - 16
d.rectangle([px(bx1 - 196), px(by1 - 18), px(bx1), px(by1)], outline=(*LINE, 255), width=px(1))
d.line([(px(bx1 - 98), px(by1 - 18)), (px(bx1 - 98), px(by1))], fill=(*LINE, 255), width=px(1))
text(d, (bx1 - 147, by1 - 8.5), "SHEET 1/1", mono(7), (*LINE, 255), "mm", tracking=1)
text(d, (bx1 - 49, by1 - 8.5), "SCALE 1:1", mono(7), (*LINE, 255), "mm", tracking=1)
# Name plates where Finder draws the icon names.
py = CY + ICON / 2 + PLATE_GAP
for cx in (AX, BX):
d.rounded_rectangle(
[px(cx - PLATE_W / 2), px(py), px(cx + PLATE_W / 2), px(py + PLATE_H)], radius=px(6), fill=(*PLATE, 255)
)
return img.convert("RGB")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--preview", help="also write the 1x and 2x PNGs to this directory")
ap.add_argument("--out", default=str(HERE / "background.tiff"))
args = ap.parse_args()
big = draw_sheet()
with tempfile.TemporaryDirectory() as tmp:
one, two = Path(tmp, "bg.png"), Path(tmp, "bg@2x.png")
big.resize((W, H), Image.LANCZOS).save(one, dpi=(72, 72))
big.resize((W * 2, H * 2), Image.LANCZOS).save(two, dpi=(144, 144))
# A multi-representation TIFF: Finder picks the 2x one on Retina screens.
raw = Path(tmp, "bg.tiff")
subprocess.run(["tiffutil", "-cat", str(one), str(two), "-out", str(raw)], check=True)
subprocess.run(["tiffutil", "-lzw", str(raw), "-out", args.out], check=True)
if args.preview:
out = Path(args.preview)
out.mkdir(parents=True, exist_ok=True)
for p in (one, two):
(out / p.name).write_bytes(p.read_bytes())
print("wrote", args.out)
if __name__ == "__main__":
main()
+41
View File
@@ -0,0 +1,41 @@
"""dmgbuild settings for the SolidCraft disk image (packaging/macos/package.sh).
Passed with -D here=<this folder> -D app=<SolidCraft.app> -D readme=<README.txt>
-D icns=<icon.icns>. dmgbuild writes the Finder layout (window, background, icon positions) straight into the image, so the build
needs no Finder session and no AppleScript.
"""
import json
import os
here = defines["here"] # noqa: F821 (this folder; settings files have no __file__)
layout = json.load(open(os.path.join(here, "layout.json")))
width, height = layout["window"]
format = "UDZO"
filesystem = "HFS+"
# `dmgbuild -D` values arrive in `defines`.
files = [defines["app"], defines["readme"]] # noqa: F821
symlinks = {"Applications": "/Applications"}
# The read-me stays on the disk for people who look for it, out of the window's calm layout.
hide = ["README.txt"]
icon = defines["icns"] # noqa: F821
background = os.path.join(here, "background.tiff")
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
default_view = "icon-view"
arrange_by = None
# The rectangle is the whole window: the title bar (32 pt on macOS 27) sits above the content area,
# which must be exactly the size of the background.
window_rect = ((260, 260), (width, height + layout["title_bar"]))
icon_size = layout["icon_size"]
text_size = layout["text_size"]
label_pos = "bottom"
icon_locations = {
os.path.basename(defines["app"]): tuple(layout["app"]), # noqa: F821
"Applications": tuple(layout["applications"]),
"README.txt": (width // 2, height + 200),
}
+1 -1
View File
@@ -1,3 +1,3 @@
# Offline authoring dependencies only; not linked into the Rust application.
numpy==2.3.5
Pillow==12.3.0
Pillow==13.0.0
+13 -4
View File
@@ -296,9 +296,10 @@ fn opaque_depth(i: TOut, front: bool) -> AoDepth {
return AoDepth(depth, bitcast<u32>(depth));
}
// Deterministic cosine-weighted hemisphere samples. Projecting the sample and a point one
// radius toward the eye supplies a depth range in both orthographic and perspective views;
// distant foreground geometry cannot cast a dark halo onto a background surface.
// Cosine-weighted hemisphere samples, the pattern turned by a different angle at each pixel.
// Projecting the sample and a point one radius toward the eye supplies a depth range in both
// orthographic and perspective views; distant foreground geometry cannot cast a dark halo onto
// a background surface.
fn occlusion(i: TOut) -> f32 {
let radius = u.ao.x;
if (radius <= 0.0 || length(i.n) < 0.5) { return 1.0; }
@@ -311,6 +312,14 @@ fn occlusion(i: TOut) -> f32 {
// The map is a 1 × 1 stand-in until a frame draws into it (the product preview draws no
// prepass): sampling it tells nothing, so leave the surface unoccluded.
if (size.x < 2 || size.y < 2) { return 1.0; }
// The same offsets at every pixel made each sample darken its own shifted copy of a nearby
// part's outline: faint ghosts of the part where the radius is large on screen (#4167),
// stepped bands along contacts where it is small (#3407). Turned by interleaved gradient
// noise (Jimenez 2014), neighbouring pixels sample different points and the copies break up
// into an even grain.
let turn = fract(52.9829189 * fract(dot(i.pos.xy, vec2<f32>(0.06711056, 0.00583715))));
let t = tangent * cos(turn * 6.28318531) + bitangent * sin(turn * 6.28318531);
let b = cross(n, t);
// One comparison for both depth directions: depths read as 0 near / 1 far (mirrored when
// the buffer runs reversed). The map is cleared to its far plane either way, so a texel
// with nothing in it reads as 1 and the `d < 1.0` test below skips it.
@@ -319,7 +328,7 @@ fn occlusion(i: TOut) -> f32 {
for (var j = 0u; j < 32u; j = j + 1u) {
let f = (f32(j) + 0.5) / 32.0;
let angle = f32(j) * 2.39996323;
let direction = tangent * (cos(angle) * sqrt(f)) + bitangent * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
let direction = t * (cos(angle) * sqrt(f)) + b * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
let reach = radius * (0.2 + 0.8 * fract(f32(j) * 0.61803399 + 0.5));
let sample = i.wp + n * (radius * 0.025) + direction * reach;
let q = u.vp * vec4<f32>(sample, 1.0);
+9 -8
View File
@@ -155,9 +155,11 @@ impl Tokens {
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
origin_point: Color32::from_rgb(214, 215, 218),
axis_x: Color32::from_rgb(255, 0, 0),
axis_y: Color32::from_rgb(0, 255, 0),
axis_z: Color32::from_rgb(2, 2, 248),
// The one set of X, Y and Z colours (#976), equal to the engine's `view::colors::AXIS_*`
// so a headless picture draws its origin axes as the window does.
axis_x: Color32::from_rgb(220, 60, 60),
axis_y: Color32::from_rgb(60, 170, 60),
axis_z: Color32::from_rgb(60, 100, 230),
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
construction_geom: icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[0]),
body: Color32::from_rgb(176, 186, 198),
@@ -178,6 +180,7 @@ impl Tokens {
body_selected: Color32::from_rgb(110, 170, 235),
sketch: Color32::from_rgb(30, 90, 200),
sketch_fixed: Color32::from_rgb(20, 20, 20),
sketch_halo: Color32::from_rgb(240, 243, 247),
sketch_point: Color32::BLACK,
sketch_locked: Color32::from_rgb(0, 140, 60),
dimension: Color32::from_rgb(36, 40, 48),
@@ -219,8 +222,6 @@ impl Tokens {
sketch_lost: Color32::from_rgb(200, 60, 90),
sketch_construction: Color32::from_rgb(230, 130, 40),
sketch_centerline: Color32::from_rgb(200, 110, 60),
triad: [Color32::from_rgb(220, 60, 60), Color32::from_rgb(60, 170, 60), Color32::from_rgb(60, 100, 230)],
gizmo: [Color32::from_rgb(225, 70, 60), Color32::from_rgb(80, 190, 80), Color32::from_rgb(70, 130, 240)],
bend_up: Color32::from_rgb(30, 140, 50),
bend_down: Color32::from_rgb(200, 90, 20),
interference: Color32::from_rgb(235, 45, 40),
@@ -336,6 +337,7 @@ impl Tokens {
t.body_edge = Color32::from_rgb(16, 16, 18);
t.sketch = Color32::from_rgb(92, 150, 255);
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
t.sketch_halo = Color32::from_rgb(28, 32, 40);
t.sketch_point = Color32::from_rgb(232, 234, 238);
t.sketch_locked = Color32::from_rgb(96, 204, 116);
t.dimension = Color32::from_rgb(224, 228, 234);
@@ -375,9 +377,8 @@ impl Tokens {
let mut t = self;
let positive = [0.20, 0.45, 0.90];
let negative = [0.85, 0.40, 0.05];
t.triad[0] = Color32::from_rgb(200, 40, 40);
t.triad[1] = Color32::from_rgb(120, 210, 80);
t.gizmo[1] = Color32::from_rgb(140, 220, 90);
t.axis_x = Color32::from_rgb(200, 40, 40);
t.axis_y = Color32::from_rgb(120, 210, 80);
t.preview_sketch = Color32::from_rgb(80, 185, 145);
t.analysis.draft = [positive, [0.95, 0.90, 0.60], negative];
t.analysis.access = [positive, negative];
+245
View File
@@ -0,0 +1,245 @@
{
"base": "light",
"colors": {
"accent": "#c63a00",
"accent_soft": "#e8d0c3",
"agent_busy": "#015ba6",
"agent_ok": "#257231",
"app_bar": "#e1ddd8",
"app_bar_hover": "#13110e14",
"app_bar_hover_button": "#13110e18",
"app_bar_hover_strong": "#13110e22",
"app_bar_hover_subtle": "#13110e0c",
"app_bar_icon": [
"#403d38",
"#b5b0a9",
"#c63a00"
],
"app_bar_separator": "#b5b0a9",
"app_bar_tab_active": "#f9f6f2",
"app_bar_text": "#403d38",
"axis_x": "#c71529",
"axis_y": "#257231",
"axis_z": "#015ba6",
"bend_down": "#705e91",
"bend_up": "#257231",
"body": "#c2beb8",
"body_edge": "#403d38",
"body_selected": "#de8d6b",
"border": "#d9d5d0",
"box_crossing": "#257231",
"box_window": "#015ba6",
"chip": "#c63a00",
"construction_geom": "#7d6535",
"construction_plane": "#7d653540",
"continuity": [
"#257231",
"#015ba6",
"#c08a1a",
"#b14578"
],
"control_border": "#d4d0ca",
"control_fill": "#fffdfa",
"cube_border": "#b5b0a9",
"cube_face": "#fffdfa",
"cube_hover": "#d4d0ca",
"cube_text": "#13110e",
"dialog_bg": "#f9f6f2",
"dialog_body": "#f9f6f2",
"dialog_border": "#d4d0ca",
"dialog_footer": "#efece8",
"dialog_label": "#403d38",
"dialog_rule": "#d9d5d0",
"dimension": "#403d38",
"dimension_driven": "#6c6863",
"error": "#c71529",
"field": "#fffdfa",
"form_accent": "#5844bc",
"form_dim": "#6c6863",
"form_error": "#c71529",
"form_error_fill": "#f4e0de",
"form_fill": "#5844bc",
"form_ink": "#13110e",
"form_primary": "#c63a00",
"form_soft": "#e4e0e5",
"form_success": "#257231",
"form_success_fill": "#e4e9df",
"form_tab_ink": "#5844bc",
"gizmo": [
"#b00018",
"#096125",
"#004c9b"
],
"glyph_bg": "#fffdfa",
"glyph_edge": "#d4d0ca",
"glyph_text": "#604b83",
"grid": "#edeae5",
"grid_major": "#e1ddd8",
"heading_bg": "#ebe7e2",
"hover": "#e1ddd7",
"hover_edge": "#13110e",
"hover_edge_halo": "#fffdfa",
"hover_profile": "#13110e22",
"hover_profile_edge": "#403d38",
"icon": "#403d38",
"icon_fill": "#d4d0ca",
"icons.access": [
"#257231",
"#c71529"
],
"icons.agent": "#5844bc",
"icons.axes": [
"#c71529",
"#257231",
"#015ba6"
],
"icons.curvature": [
"#015ba6",
"#257231",
"#c71529"
],
"icons.draft": [
"#257231",
"#e6af2b"
],
"icons.floor": "#6c6863",
"icons.fold": "#8d8982",
"icons.folder": "#c9a96a",
"icons.glint": "#ffffffa0",
"icons.highlight": "#fffdfa",
"icons.interference": "#c71529",
"icons.marker_halo": "#13110e96",
"icons.paint": [
"#c71529",
"#015ba6",
"#e6af2b"
],
"icons.plane": "#e3cf9f",
"icons.sheet": "#b9cde2",
"icons.sheet_2": "#a5bfd9",
"icons.sheet_light": "#dbe6f0",
"icons.sheet_line": "#015ba6",
"icons.sky": "#b9cde2",
"icons.stripe": "#13110e",
"icons.warning": "#e6af2b",
"interference": "#c71529",
"logo": [
"#bb461e",
"#c63a00",
"#902e04"
],
"manipulator": "#004c9b",
"mesh_accent": "#7e6900",
"mesh_fill": "#7e6900",
"mesh_ink": "#6b5903",
"mesh_soft": "#e7e3d8",
"on_accent": "#fffbed",
"origin_plane": "#6c686338",
"origin_plane_edge": "#6c6863c8",
"origin_plane_hover": "#015ba670",
"origin_point": "#403d38",
"overlay": "#fffdfaeb",
"panel": "#efece8",
"panel_header": "#ebe7e2",
"preview_add": "#4f8fd6",
"preview_cut": "#c715296e",
"preview_new": "#e6af2b",
"preview_sketch": "#2d713b",
"profile_fill": "#015ba614",
"render_accent": "#ac1f6a",
"render_fill": "#ac1f6a",
"render_ink": "#ac1f6a",
"render_soft": "#eadedf",
"rubber_band": "#004c9b",
"section_cap": "#7d6535",
"sel_edge": "#c63a00",
"sel_edge_rim": "#e29c80",
"sel_face": "#d67147",
"sel_profile": "#c63a00a0",
"sel_profile_edge": "#c63a00",
"sel_vertex": "#c63a00",
"selected_bg": "#f1d8cb",
"selected_ink": "#13110e",
"sheet_fill": "#015ba6",
"sheet_ink": "#015ba6",
"sheet_soft": "#dee2e3",
"sketch": "#015ba6",
"sketch_accent": "#2d713b",
"sketch_centerline": "#403d38",
"sketch_construction": "#7d6535",
"sketch_fixed": "#13110e",
"sketch_ink": "#2d713b",
"sketch_locked": "#257231",
"sketch_lost": "#c71529",
"sketch_point": "#13110e",
"sketch_point_free": "#015ba6",
"sketch_projected": "#705e91",
"sketch_soft": "#e1e3dc",
"subheading_bg": "#efece8",
"surface_accent": "#7d6535",
"surface_fill": "#7d6535",
"surface_ink": "#6a562e",
"surface_soft": "#e7e3db",
"tab_on_fill_dark": "#13110e",
"text": "#13110e",
"text_dim": "#6c6863",
"timeline": "#efece8",
"timeline_item": "#fffdfa",
"timeline_item_error": "#f2c5c8",
"timeline_marker": "#c63a00",
"toolbar": "#efece8",
"triad": [
"#c71529",
"#257231",
"#015ba6"
],
"viewport_bottom": "#f1ede8",
"viewport_top": "#f9f6f2",
"warning": "#956000"
},
"format": "solidcraft-theme/1",
"name": "Akari",
"icon_palette": {
"ink": "#13110e",
"line": "#505054",
"passive-1": "#dee5ee",
"passive-2": "#aeb5bd",
"passive-3": "#81878f",
"guide": "#6e6e73",
"solid": [
"#dbe9fe",
"#81b3fa",
"#1875eb"
],
"surface": [
"#fde3c4",
"#daa662",
"#a06f28"
],
"mesh": [
"#fee7a2",
"#d1ad37",
"#937717"
],
"form": [
"#ede2ff",
"#c39afc",
"#8b62bf"
],
"sketch": [
"#b1fdc5",
"#72c78b",
"#388e56"
],
"constraint": [
"#fee0dd",
"#f98e87",
"#d73339"
],
"construction": [
"#fde3c4",
"#daa662",
"#a06f28"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#d4902f",
"form_success_fill": "#3b280d",
"form_tab_ink": "#cf8d31",
"gizmo": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"glyph_bg": "#201607",
"glyph_edge": "#e0d1ac",
"glyph_text": "#e0d1ac",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ba7e29",
"sketch_construction": "#ad7626",
"sketch_fixed": "#c68b2f",
"sketch_halo": "#170d03",
"sketch_ink": "#f7ce88",
"sketch_locked": "#d4902f",
"sketch_lost": "#fff0ca",
@@ -188,11 +184,6 @@
"timeline_item_error": "#46300f",
"timeline_marker": "#9b6922",
"toolbar": "#100b04",
"triad": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"viewport_bottom": "#090603",
"viewport_top": "#090603",
"warning": "#ffd274"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#6ee0bb",
"form_success_fill": "#153e47",
"form_tab_ink": "#b6a9e4",
"gizmo": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"glyph_bg": "#0a233e",
"glyph_edge": "#ff968d",
"glyph_text": "#ff968d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e7b374",
"sketch_construction": "#78b8dc",
"sketch_fixed": "#e3f0fa",
"sketch_halo": "#092c50",
"sketch_ink": "#9ddab7",
"sketch_locked": "#6ee0bb",
"sketch_lost": "#ff968d",
@@ -188,11 +184,6 @@
"timeline_item_error": "#422d40",
"timeline_marker": "#94b2cb",
"toolbar": "#0c223c",
"triad": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"viewport_bottom": "#143b67",
"viewport_top": "#143b67",
"warning": "#f0d18c"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a6e3a1",
"form_success_fill": "#353f42",
"form_tab_ink": "#b4befe",
"gizmo": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"glyph_bg": "#313244",
"glyph_edge": "#fc93b0",
"glyph_text": "#fc93b0",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f9e2af",
"sketch_construction": "#f9e2af",
"sketch_fixed": "#cdd6f4",
"sketch_halo": "#11111b",
"sketch_ink": "#a6e3a1",
"sketch_locked": "#a6e3a1",
"sketch_lost": "#f38ba8",
@@ -188,11 +184,6 @@
"timeline_item_error": "#51384b",
"timeline_marker": "#a6adc8",
"toolbar": "#1e1e2e",
"triad": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"viewport_bottom": "#11111b",
"viewport_top": "#11111b",
"warning": "#f9e2af"
+275
View File
@@ -0,0 +1,275 @@
{
"base": "dark",
"colors": {
"accent": "#e8845e",
"accent_soft": "#44281d",
"agent_busy": "#6daff7",
"agent_ok": "#89cf8e",
"app_bar": "#1b1814",
"app_bar_hover": "#e4e1dd14",
"app_bar_hover_button": "#e4e1dd18",
"app_bar_hover_strong": "#e4e1dd22",
"app_bar_hover_subtle": "#e4e1dd0c",
"app_bar_icon": [
"#c3bdb5",
"#706354",
"#e8845e"
],
"app_bar_separator": "#5d5246",
"app_bar_tab_active": "#29241f",
"app_bar_text": "#c3bdb5",
"axis_x": "#f85052",
"axis_y": "#89cf8e",
"axis_z": "#6daff7",
"bend_down": "#ab9acd",
"bend_up": "#89cf8e",
"body": "#8b8378",
"body_edge": "#1b1814",
"body_selected": "#b8603f",
"border": "#403930",
"box_crossing": "#89cf8e",
"box_window": "#6daff7",
"chip": "#e8845e",
"construction_geom": "#ceb687",
"construction_plane": "#ceb68740",
"continuity": [
"#89cf8e",
"#6daff7",
"#e6af2b",
"#f38bb7"
],
"control_border": "#5d5246",
"control_fill": "#3c352d",
"cube_border": "#706354",
"cube_face": "#3c352d",
"cube_hover": "#5d5246",
"cube_text": "#e4e1dd",
"dialog_bg": "#312b25",
"dialog_body": "#312b25",
"dialog_border": "#4b4238",
"dialog_footer": "#29241f",
"dialog_label": "#c3bdb5",
"dialog_rule": "#403930",
"dimension": "#c3bdb5",
"dimension_driven": "#9d9285",
"error": "#f85052",
"field": "#3c352d",
"form_accent": "#a6a2fc",
"form_dim": "#9d9285",
"form_error": "#ff6060",
"form_error_fill": "#37292b",
"form_fill": "#a6a2fc",
"form_ink": "#ece6d9",
"form_primary": "#e8845e",
"form_soft": "#2f3043",
"form_success": "#89cf8e",
"form_success_fill": "#2c3631",
"form_tab_ink": "#a6a2fc",
"gizmo": [
"#ff6060",
"#98dfa2",
"#7abfff"
],
"glyph_bg": "#312b25",
"glyph_edge": "#5d5246",
"glyph_text": "#ab9acd",
"grid": "#373029",
"grid_major": "#3c352d",
"heading_bg": "#221e1a",
"hover": "#4b4238",
"hover_edge": "#1b1814",
"hover_edge_halo": "#e4e1dd",
"hover_profile": "#e4e1dd22",
"hover_profile_edge": "#c3bdb5",
"icon": "#c3bdb5",
"icon_fill": "#5d5246",
"icons.access": [
"#89cf8e",
"#f85052"
],
"icons.agent": "#aba3ff",
"icons.axes": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"icons.curvature": [
"#6daff7",
"#89cf8e",
"#f85052"
],
"icons.draft": [
"#89cf8e",
"#e6af2b"
],
"icons.floor": "#4b4238",
"icons.fold": "#8a7b68",
"icons.folder": "#ceb687",
"icons.glint": "#fdfcfca0",
"icons.highlight": "#e4e1dd",
"icons.interference": "#f85052",
"icons.marker_halo": "#1b181496",
"icons.paint": [
"#f85052",
"#6daff7",
"#e6af2b"
],
"icons.plane": "#dcc9a0",
"icons.sheet": "#a2bedb",
"icons.sheet_2": "#8fb0d2",
"icons.sheet_light": "#dcd8d3",
"icons.sheet_line": "#6daff7",
"icons.sky": "#a2bedb",
"icons.stripe": "#1b1814",
"icons.warning": "#e6af2b",
"interference": "#f85052",
"logo": [
"#f0a07f",
"#e8845e",
"#b1573a"
],
"manipulator": "#7abfff",
"mesh_accent": "#cfc152",
"mesh_fill": "#cfc152",
"mesh_ink": "#cfc152",
"mesh_soft": "#363624",
"on_accent": "#1d1a17",
"origin_plane": "#9d928538",
"origin_plane_edge": "#9d9285c8",
"origin_plane_hover": "#6daff770",
"origin_point": "#c3bdb5",
"overlay": "#221e1af0",
"panel": "#221e1a",
"panel_header": "#312b25",
"preview_add": "#4c78a7",
"preview_cut": "#f850526e",
"preview_new": "#e6af2b",
"preview_sketch": "#8eca96",
"profile_fill": "#6daff71c",
"render_accent": "#f38bb7",
"render_fill": "#f38bb7",
"render_ink": "#f38bb7",
"render_soft": "#3d2c36",
"rubber_band": "#7abfff",
"section_cap": "#ceb687",
"sel_edge": "#e8845e",
"sel_edge_rim": "#f5b9a0",
"sel_face": "#d4693f",
"sel_profile": "#e8845eb0",
"sel_profile_edge": "#e8845e",
"sel_vertex": "#e8845e",
"selected_bg": "#4d3329",
"selected_ink": "#ece6d9",
"sheet_fill": "#6daff7",
"sheet_ink": "#6daff7",
"sheet_soft": "#253242",
"sketch": "#6daff7",
"sketch_accent": "#8eca96",
"sketch_centerline": "#c3bdb5",
"sketch_construction": "#ceb687",
"sketch_fixed": "#e4e1dd",
"sketch_ink": "#8eca96",
"sketch_locked": "#89cf8e",
"sketch_lost": "#f85052",
"sketch_point": "#e4e1dd",
"sketch_point_free": "#6daff7",
"sketch_projected": "#998cb3",
"sketch_soft": "#2b3730",
"subheading_bg": "#29241f",
"surface_accent": "#ceb687",
"surface_fill": "#ceb687",
"surface_ink": "#ceb687",
"surface_soft": "#36342e",
"tab_on_fill_dark": "#1b1814",
"text": "#ece6d9",
"text_dim": "#9d9484",
"timeline": "#221e1a",
"timeline_item": "#312b25",
"timeline_item_error": "#552f31",
"timeline_marker": "#e8845e",
"toolbar": "#221e1a",
"triad": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"viewport_bottom": "#29241f",
"viewport_top": "#312b25",
"warning": "#e6af2b"
},
"format": "solidcraft-theme/1",
"name": "Clay Dark",
"radii": {
"radius_sm": 6,
"radius_md": 9,
"radius_lg": 13,
"radius_xl": 18,
"control_radius": 9,
"sketch_mode_radius": 10
},
"borders": {
"primary": {
"width": 1,
"color": "#4a433b"
},
"secondary": {
"width": 1,
"color": "#3b362f"
},
"tertiary": {
"width": 1,
"color": "#332f2a"
},
"container": {
"width": 1,
"color": "#443d35"
},
"control": {
"width": 1,
"color": "#4a433b"
}
},
"icon_palette": {
"ink": "#262320",
"line": "#ece6d9",
"passive-1": "#c5bdae",
"passive-2": "#9a9283",
"passive-3": "#6f6859",
"guide": "#a6a093",
"solid": [
"#a0d7fb",
"#65bdf3",
"#4aa3d8"
],
"surface": [
"#fcc28e",
"#dea572",
"#c38c59"
],
"mesh": [
"#e8cd7d",
"#cab060",
"#b19747"
],
"form": [
"#d5c4fd",
"#b9a7df",
"#a08fc4"
],
"sketch": [
"#b0dbc0",
"#94bea4",
"#7ba58b"
],
"constraint": [
"#febbb9",
"#fb8e8d",
"#e57071"
],
"construction": [
"#fcc28e",
"#dea572",
"#c38c59"
]
}
}
+275
View File
@@ -0,0 +1,275 @@
{
"base": "light",
"colors": {
"accent": "#bf5634",
"accent_soft": "#f0d8ca",
"agent_busy": "#015ba6",
"agent_ok": "#257231",
"app_bar": "#e6dfd3",
"app_bar_hover": "#14110d14",
"app_bar_hover_button": "#14110d18",
"app_bar_hover_strong": "#14110d22",
"app_bar_hover_subtle": "#14110d0c",
"app_bar_icon": [
"#423e36",
"#b7b1a7",
"#bf5634"
],
"app_bar_separator": "#b7b1a7",
"app_bar_tab_active": "#faf7f1",
"app_bar_text": "#423e36",
"axis_x": "#c71529",
"axis_y": "#257231",
"axis_z": "#015ba6",
"bend_down": "#705e91",
"bend_up": "#257231",
"body": "#c8c0b3",
"body_edge": "#423e36",
"body_selected": "#e2967a",
"border": "#e0d8c9",
"box_crossing": "#257231",
"box_window": "#015ba6",
"chip": "#bf5634",
"construction_geom": "#7d6535",
"construction_plane": "#7d653540",
"continuity": [
"#257231",
"#015ba6",
"#c08a1a",
"#b14578"
],
"control_border": "#dcd3c2",
"control_fill": "#fdfaf3",
"cube_border": "#b7b1a7",
"cube_face": "#fdfaf3",
"cube_hover": "#dcd3c2",
"cube_text": "#14110d",
"dialog_bg": "#faf7f1",
"dialog_body": "#faf7f1",
"dialog_border": "#dcd3c2",
"dialog_footer": "#f4eee3",
"dialog_label": "#423e36",
"dialog_rule": "#e0d8c9",
"dimension": "#423e36",
"dimension_driven": "#726a5d",
"error": "#c71529",
"field": "#fdfaf3",
"form_accent": "#5844bc",
"form_dim": "#726a5d",
"form_error": "#c71529",
"form_error_fill": "#f4e0de",
"form_fill": "#5844bc",
"form_ink": "#2a251f",
"form_primary": "#bf5634",
"form_soft": "#e4e0e5",
"form_success": "#257231",
"form_success_fill": "#e4e9df",
"form_tab_ink": "#5844bc",
"gizmo": [
"#b00018",
"#096125",
"#004c9b"
],
"glyph_bg": "#fdfaf3",
"glyph_edge": "#dcd3c2",
"glyph_text": "#604b83",
"grid": "#f3ecdf",
"grid_major": "#e6dfd3",
"heading_bg": "#f2e9db",
"hover": "#e6dfd2",
"hover_edge": "#14110d",
"hover_edge_halo": "#fefdfb",
"hover_profile": "#14110d22",
"hover_profile_edge": "#423e36",
"icon": "#423e36",
"icon_fill": "#dcd3c2",
"icons.access": [
"#257231",
"#c71529"
],
"icons.agent": "#5844bc",
"icons.axes": [
"#c71529",
"#257231",
"#015ba6"
],
"icons.curvature": [
"#015ba6",
"#257231",
"#c71529"
],
"icons.draft": [
"#257231",
"#e6af2b"
],
"icons.floor": "#726a5d",
"icons.fold": "#938b7c",
"icons.folder": "#c9a96a",
"icons.glint": "#ffffffa0",
"icons.highlight": "#fefdfb",
"icons.interference": "#c71529",
"icons.marker_halo": "#14110d96",
"icons.paint": [
"#c71529",
"#015ba6",
"#e6af2b"
],
"icons.plane": "#e3cf9f",
"icons.sheet": "#b9cde2",
"icons.sheet_2": "#a5bfd9",
"icons.sheet_light": "#f1e9da",
"icons.sheet_line": "#015ba6",
"icons.sky": "#b9cde2",
"icons.stripe": "#14110d",
"icons.warning": "#e6af2b",
"interference": "#c71529",
"logo": [
"#d0704b",
"#bf5634",
"#92411f"
],
"manipulator": "#004c9b",
"mesh_accent": "#7e6900",
"mesh_fill": "#7e6900",
"mesh_ink": "#6b5903",
"mesh_soft": "#e7e3d8",
"on_accent": "#fffefb",
"origin_plane": "#726a5d38",
"origin_plane_edge": "#726a5dc8",
"origin_plane_hover": "#015ba670",
"origin_point": "#423e36",
"overlay": "#fefdfbeb",
"panel": "#f4eee3",
"panel_header": "#f2e9db",
"preview_add": "#4f8fd6",
"preview_cut": "#c715296e",
"preview_new": "#e6af2b",
"preview_sketch": "#2d713b",
"profile_fill": "#015ba614",
"render_accent": "#ac1f6a",
"render_fill": "#ac1f6a",
"render_ink": "#ac1f6a",
"render_soft": "#eadedf",
"rubber_band": "#004c9b",
"section_cap": "#7d6535",
"sel_edge": "#bf5634",
"sel_edge_rim": "#e8a98b",
"sel_face": "#d4714c",
"sel_profile": "#bf5634a0",
"sel_profile_edge": "#bf5634",
"sel_vertex": "#bf5634",
"selected_bg": "#f3ddd0",
"selected_ink": "#2a251f",
"sheet_fill": "#015ba6",
"sheet_ink": "#015ba6",
"sheet_soft": "#dee2e3",
"sketch": "#015ba6",
"sketch_accent": "#2d713b",
"sketch_centerline": "#423e36",
"sketch_construction": "#7d6535",
"sketch_fixed": "#14110d",
"sketch_ink": "#2d713b",
"sketch_locked": "#257231",
"sketch_lost": "#c71529",
"sketch_point": "#14110d",
"sketch_point_free": "#015ba6",
"sketch_projected": "#705e91",
"sketch_soft": "#e1e3dc",
"subheading_bg": "#f4eee3",
"surface_accent": "#7d6535",
"surface_fill": "#7d6535",
"surface_ink": "#6a562e",
"surface_soft": "#e7e3db",
"tab_on_fill_dark": "#14110d",
"text": "#2a251f",
"text_dim": "#675e50",
"timeline": "#f4eee3",
"timeline_item": "#fdfaf3",
"timeline_item_error": "#f2c5c8",
"timeline_marker": "#bf5634",
"toolbar": "#f4eee3",
"triad": [
"#c71529",
"#257231",
"#015ba6"
],
"viewport_bottom": "#ebe4d4",
"viewport_top": "#f7f2e8",
"warning": "#956000"
},
"format": "solidcraft-theme/1",
"name": "Clay",
"radii": {
"radius_sm": 6,
"radius_md": 9,
"radius_lg": 13,
"radius_xl": 18,
"control_radius": 9,
"sketch_mode_radius": 10
},
"borders": {
"primary": {
"width": 1,
"color": "#cfc4af"
},
"secondary": {
"width": 1,
"color": "#ddd3c0"
},
"tertiary": {
"width": 1,
"color": "#e3dbca"
},
"container": {
"width": 1,
"color": "#d6cbb7"
},
"control": {
"width": 1,
"color": "#d0c5b0"
}
},
"icon_palette": {
"ink": "#2a251f",
"line": "#4b4338",
"passive-1": "#e6e0d4",
"passive-2": "#b9b0a0",
"passive-3": "#8a8272",
"guide": "#7a7263",
"solid": [
"#dbe9fe",
"#81b3fa",
"#1875eb"
],
"surface": [
"#fde3c4",
"#daa662",
"#a06f28"
],
"mesh": [
"#fee7a2",
"#d1ad37",
"#937717"
],
"form": [
"#ede2ff",
"#c39afc",
"#8b62bf"
],
"sketch": [
"#b1fdc5",
"#72c78b",
"#388e56"
],
"constraint": [
"#fee0dd",
"#f98e87",
"#d73339"
],
"construction": [
"#fde3c4",
"#daa662",
"#a06f28"
]
}
}
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#b4b4b8",
"app_bar_tab_active": "#f4f4f4",
"app_bar_text": "#242426",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#e95049",
"axis_y": "#35a754",
"axis_z": "#297fdb",
"bend_down": "#bd843f",
"bend_up": "#36934d",
"body": "#b6bbc3",
@@ -65,11 +65,6 @@
"form_success": "#237b38",
"form_success_fill": "#e6f3e8",
"form_tab_ink": "#75559f",
"gizmo": [
"#ed514b",
"#36ad59",
"#2184e9"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#d73339",
"glyph_text": "#d73339",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b77c47",
"sketch_construction": "#cb8b37",
"sketch_fixed": "#262628",
"sketch_halo": "#ffffff",
"sketch_ink": "#236f40",
"sketch_locked": "#318348",
"sketch_lost": "#c94f68",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f8dedb",
"timeline_marker": "#63636b",
"toolbar": "#ececec",
"triad": [
"#e95049",
"#35a754",
"#297fdb"
],
"viewport_bottom": "#d9dce2",
"viewport_top": "#f5f5f7",
"warning": "#ad7400"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#83c69b",
"form_success_fill": "#35423e",
"form_tab_ink": "#a796cb",
"gizmo": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"glyph_bg": "#303338",
"glyph_edge": "#e57071",
"glyph_text": "#e57071",
@@ -168,6 +163,7 @@
"sketch_centerline": "#dbb879",
"sketch_construction": "#dbb879",
"sketch_fixed": "#e0e2e5",
"sketch_halo": "#17191d",
"sketch_ink": "#88b298",
"sketch_locked": "#83c69b",
"sketch_lost": "#ed7777",
@@ -188,11 +184,6 @@
"timeline_item_error": "#553a3d",
"timeline_marker": "#a0a6b0",
"toolbar": "#25272b",
"triad": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"viewport_bottom": "#17191d",
"viewport_top": "#17191d",
"warning": "#dbb879"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#39d45c",
"form_success_fill": "#133b20",
"form_tab_ink": "#36b95c",
"gizmo": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"glyph_bg": "#09200e",
"glyph_edge": "#afd5b6",
"glyph_text": "#afd5b6",
@@ -168,6 +163,7 @@
"sketch_centerline": "#32ba50",
"sketch_construction": "#2fad4a",
"sketch_fixed": "#8def9f",
"sketch_halo": "#021007",
"sketch_ink": "#bbffca",
"sketch_locked": "#39d45c",
"sketch_lost": "#d8ffdf",
@@ -188,11 +184,6 @@
"timeline_item_error": "#21462a",
"timeline_marker": "#5b9b6a",
"toolbar": "#061009",
"triad": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"viewport_bottom": "#010703",
"viewport_top": "#010703",
"warning": "#d1ffad"
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#141617",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +84,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#382c20",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#4e5954",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#4a3a2c",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#48544f",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#423326",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fbf8e3",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f3e6c6",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#f6edd0",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#edd7ad",
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fcf5d8",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f5e2b7",
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#40e080",
"form_success_fill": "#0b2616",
"form_tab_ink": "#df8fff",
"gizmo": [
"#ff6666",
"#40e080",
"#0070d0"
],
"glyph_bg": "#151515",
"glyph_edge": "#fa8a86",
"glyph_text": "#fa8a86",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ffcc00",
"sketch_construction": "#ffcc00",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#00ff80",
"sketch_locked": "#40e080",
"sketch_lost": "#ff6666",
@@ -188,11 +184,6 @@
"timeline_item_error": "#3d1818",
"timeline_marker": "#cccccc",
"toolbar": "#000000",
"triad": [
"#ff6666",
"#40e080",
"#0070d0"
],
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffcc00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a3be8c",
"form_success_fill": "#424b4d",
"form_tab_ink": "#c3a5bd",
"gizmo": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"glyph_bg": "#3b4252",
"glyph_edge": "#f9959c",
"glyph_text": "#f9959c",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ebcb8b",
"sketch_construction": "#ebcb8b",
"sketch_fixed": "#eceff4",
"sketch_halo": "#242f40",
"sketch_ink": "#a3be8c",
"sketch_locked": "#a3be8c",
"sketch_lost": "#bf616a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#513f4a",
"timeline_marker": "#a4b0c2",
"toolbar": "#2e3440",
"triad": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"viewport_bottom": "#242f40",
"viewport_top": "#242f40",
"warning": "#ebcb8b"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#50fa7b",
"form_success_fill": "#2f4d42",
"form_tab_ink": "#d6acff",
"gizmo": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"glyph_bg": "#343746",
"glyph_edge": "#fc827b",
"glyph_text": "#fc827b",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f1fa8c",
"sketch_construction": "#f1fa8c",
"sketch_fixed": "#f8f8f2",
"sketch_halo": "#1d1e29",
"sketch_ink": "#50fa7b",
"sketch_locked": "#50fa7b",
"sketch_lost": "#ff5555",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#a1a4c4",
"toolbar": "#282a36",
"triad": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"viewport_bottom": "#1d1e29",
"viewport_top": "#1d1e29",
"warning": "#f1fa8c"
+245
View File
@@ -0,0 +1,245 @@
{
"base": "dark",
"colors": {
"accent": "#ff7c50",
"accent_soft": "#3a2723",
"agent_busy": "#6daff7",
"agent_ok": "#89cf8e",
"app_bar": "#0f1113",
"app_bar_hover": "#e1e3e514",
"app_bar_hover_button": "#e1e3e518",
"app_bar_hover_strong": "#e1e3e522",
"app_bar_hover_subtle": "#e1e3e50c",
"app_bar_icon": [
"#bbbec1",
"#585b5f",
"#ff7c50"
],
"app_bar_separator": "#474b4f",
"app_bar_tab_active": "#1b1e20",
"app_bar_text": "#bbbec1",
"axis_x": "#f85052",
"axis_y": "#89cf8e",
"axis_z": "#6daff7",
"bend_down": "#ab9acd",
"bend_up": "#89cf8e",
"body": "#74777b",
"body_edge": "#0f1113",
"body_selected": "#c56241",
"border": "#2f3235",
"box_crossing": "#89cf8e",
"box_window": "#6daff7",
"chip": "#ff7c50",
"construction_geom": "#ceb687",
"construction_plane": "#ceb68740",
"continuity": [
"#89cf8e",
"#6daff7",
"#e6af2b",
"#f38bb7"
],
"control_border": "#474b4f",
"control_fill": "#2c2e31",
"cube_border": "#585b5f",
"cube_face": "#2c2e31",
"cube_hover": "#474b4f",
"cube_text": "#e1e3e5",
"dialog_bg": "#222527",
"dialog_body": "#222527",
"dialog_border": "#383b3e",
"dialog_footer": "#1b1e20",
"dialog_label": "#bbbec1",
"dialog_rule": "#2f3235",
"dimension": "#bbbec1",
"dimension_driven": "#8f9296",
"error": "#f85052",
"field": "#2c2e31",
"form_accent": "#a6a2fc",
"form_dim": "#8f9296",
"form_error": "#ff6060",
"form_error_fill": "#37292b",
"form_fill": "#a6a2fc",
"form_ink": "#e1e3e5",
"form_primary": "#ff7c50",
"form_soft": "#2f3043",
"form_success": "#89cf8e",
"form_success_fill": "#2c3631",
"form_tab_ink": "#a6a2fc",
"gizmo": [
"#ff6060",
"#98dfa2",
"#7abfff"
],
"glyph_bg": "#222527",
"glyph_edge": "#474b4f",
"glyph_text": "#ab9acd",
"grid": "#27292c",
"grid_major": "#2c2e31",
"heading_bg": "#15171a",
"hover": "#383b3e",
"hover_edge": "#0f1113",
"hover_edge_halo": "#e1e3e5",
"hover_profile": "#e1e3e522",
"hover_profile_edge": "#bbbec1",
"icon": "#bbbec1",
"icon_fill": "#474b4f",
"icons.access": [
"#89cf8e",
"#f85052"
],
"icons.agent": "#aba3ff",
"icons.axes": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"icons.curvature": [
"#6daff7",
"#89cf8e",
"#f85052"
],
"icons.draft": [
"#89cf8e",
"#e6af2b"
],
"icons.floor": "#383b3e",
"icons.fold": "#707376",
"icons.folder": "#ceb687",
"icons.glint": "#ffffffa0",
"icons.highlight": "#e1e3e5",
"icons.interference": "#f85052",
"icons.marker_halo": "#0f111396",
"icons.paint": [
"#f85052",
"#6daff7",
"#e6af2b"
],
"icons.plane": "#dcc9a0",
"icons.sheet": "#a2bedb",
"icons.sheet_2": "#8fb0d2",
"icons.sheet_light": "#c9d9ea",
"icons.sheet_line": "#6daff7",
"icons.sky": "#a2bedb",
"icons.stripe": "#0f1113",
"icons.warning": "#e6af2b",
"interference": "#f85052",
"logo": [
"#ff9c7c",
"#ff7c50",
"#b75c3e"
],
"manipulator": "#7abfff",
"mesh_accent": "#cfc152",
"mesh_fill": "#cfc152",
"mesh_ink": "#cfc152",
"mesh_soft": "#363624",
"on_accent": "#0f1113",
"origin_plane": "#8f929638",
"origin_plane_edge": "#8f9296c8",
"origin_plane_hover": "#6daff770",
"origin_point": "#bbbec1",
"overlay": "#15171af0",
"panel": "#15171a",
"panel_header": "#222527",
"preview_add": "#4c78a7",
"preview_cut": "#f850526e",
"preview_new": "#e6af2b",
"preview_sketch": "#8eca96",
"profile_fill": "#6daff71c",
"render_accent": "#f38bb7",
"render_fill": "#f38bb7",
"render_ink": "#f38bb7",
"render_soft": "#3d2c36",
"rubber_band": "#7abfff",
"section_cap": "#ceb687",
"sel_edge": "#ff7c50",
"sel_edge_rim": "#ffbea8",
"sel_face": "#dd6d47",
"sel_profile": "#ff7c50b0",
"sel_profile_edge": "#ff7c50",
"sel_vertex": "#ff7c50",
"selected_bg": "#45332e",
"selected_ink": "#e1e3e5",
"sheet_fill": "#6daff7",
"sheet_ink": "#6daff7",
"sheet_soft": "#253242",
"sketch": "#6daff7",
"sketch_accent": "#8eca96",
"sketch_centerline": "#bbbec1",
"sketch_construction": "#ceb687",
"sketch_fixed": "#e1e3e5",
"sketch_ink": "#8eca96",
"sketch_locked": "#89cf8e",
"sketch_lost": "#f85052",
"sketch_point": "#e1e3e5",
"sketch_point_free": "#6daff7",
"sketch_projected": "#998cb3",
"sketch_soft": "#2b3730",
"subheading_bg": "#1b1e20",
"surface_accent": "#ceb687",
"surface_fill": "#ceb687",
"surface_ink": "#ceb687",
"surface_soft": "#36342e",
"tab_on_fill_dark": "#0f1113",
"text": "#e1e3e5",
"text_dim": "#8f9296",
"timeline": "#15171a",
"timeline_item": "#222527",
"timeline_item_error": "#552f31",
"timeline_marker": "#ff7c50",
"toolbar": "#15171a",
"triad": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"viewport_bottom": "#1b1e20",
"viewport_top": "#222527",
"warning": "#e6af2b"
},
"format": "solidcraft-theme/1",
"name": "Okibi",
"icon_palette": {
"ink": "#15171a",
"line": "#e1e3e5",
"passive-1": "#bfc1c5",
"passive-2": "#939599",
"passive-3": "#6a6c6f",
"guide": "#a0a6b0",
"solid": [
"#a0d7fb",
"#65bdf3",
"#4aa3d8"
],
"surface": [
"#fcc28e",
"#dea572",
"#c38c59"
],
"mesh": [
"#e8cd7d",
"#cab060",
"#b19747"
],
"form": [
"#d5c4fd",
"#b9a7df",
"#a08fc4"
],
"sketch": [
"#b0dbc0",
"#94bea4",
"#7ba58b"
],
"constraint": [
"#febbb9",
"#fb8e8d",
"#e57071"
],
"construction": [
"#fcc28e",
"#dea572",
"#c38c59"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#8fdb9a",
"form_success_fill": "#2c4a33",
"form_tab_ink": "#c97fdf",
"gizmo": [
"#e06c75",
"#98c379",
"#61afef"
],
"glyph_bg": "#282c34",
"glyph_edge": "#f57f87",
"glyph_text": "#f57f87",
@@ -168,6 +163,7 @@
"sketch_centerline": "#d19a66",
"sketch_construction": "#d19a66",
"sketch_fixed": "#abb2bf",
"sketch_halo": "#181a1f",
"sketch_ink": "#98c379",
"sketch_locked": "#98c379",
"sketch_lost": "#e06c75",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#abb2bf",
"toolbar": "#21252b",
"triad": [
"#e06c75",
"#98c379",
"#61afef"
],
"viewport_bottom": "#282c34",
"viewport_top": "#282c34",
"warning": "#e5c07b"
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#aaaaaa",
"app_bar_tab_active": "#e6e6e6",
"app_bar_text": "#222222",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#dc3c3c",
"axis_y": "#3caa3c",
"axis_z": "#3c64e6",
"bend_down": "#c85a14",
"bend_up": "#1e8c32",
"body": "#adadb6",
@@ -65,11 +65,6 @@
"form_success": "#336633",
"form_success_fill": "#e2ecdf",
"form_tab_ink": "#6d4e7d",
"gizmo": [
"#e1463c",
"#50be50",
"#4682f0"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#ab2125",
"glyph_text": "#ab2125",
@@ -168,6 +163,7 @@
"sketch_centerline": "#c86e3c",
"sketch_construction": "#e68228",
"sketch_fixed": "#222222",
"sketch_halo": "#ffffff",
"sketch_ink": "#456045",
"sketch_locked": "#447744",
"sketch_lost": "#c83c5a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f2dddd",
"timeline_marker": "#666666",
"toolbar": "#dddddd",
"triad": [
"#dc3c3c",
"#3caa3c",
"#3c64e6"
],
"viewport_bottom": "#ffffff",
"viewport_top": "#ffffff",
"warning": "#997722"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#859900",
"form_success_fill": "#dcdbb1",
"form_tab_ink": "#565a9d",
"gizmo": [
"#dc322f",
"#859900",
"#346e88"
],
"glyph_bg": "#e3ddc9",
"glyph_edge": "#da2f2d",
"glyph_text": "#da2f2d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b58900",
"sketch_construction": "#b58900",
"sketch_fixed": "#586e75",
"sketch_halo": "#fdf6e3",
"sketch_ink": "#5a6800",
"sketch_locked": "#859900",
"sketch_lost": "#dc322f",
@@ -188,11 +184,6 @@
"timeline_item_error": "#eabcad",
"timeline_marker": "#657b83",
"toolbar": "#eee8d5",
"triad": [
"#dc322f",
"#859900",
"#346e88"
],
"viewport_bottom": "#fdf6e3",
"viewport_top": "#fdf6e3",
"warning": "#b58900"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#008000",
"form_success_fill": "#c0dcc0",
"form_tab_ink": "#7d007d",
"gizmo": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#800000",
"glyph_text": "#800000",
@@ -168,6 +163,7 @@
"sketch_centerline": "#808000",
"sketch_construction": "#808000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
"sketch_ink": "#005400",
"sketch_locked": "#008000",
"sketch_lost": "#800080",
@@ -188,11 +184,6 @@
"timeline_item_error": "#ffffc0",
"timeline_marker": "#000000",
"toolbar": "#c0c0c0",
"triad": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"viewport_bottom": "#008080",
"viewport_top": "#008080",
"warning": "#ffff00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#9ece6a",
"form_success_fill": "#303932",
"form_tab_ink": "#bb9af7",
"gizmo": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"glyph_bg": "#24283b",
"glyph_edge": "#f6758d",
"glyph_text": "#f6758d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e0af68",
"sketch_construction": "#e0af68",
"sketch_fixed": "#c0caf5",
"sketch_halo": "#151722",
"sketch_ink": "#9ece6a",
"sketch_locked": "#9ece6a",
"sketch_lost": "#f7768e",
@@ -188,11 +184,6 @@
"timeline_item_error": "#4f313f",
"timeline_marker": "#9aa5ce",
"toolbar": "#1a1b26",
"triad": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"viewport_bottom": "#151722",
"viewport_top": "#151722",
"warning": "#e0af68"
+21 -12
View File
@@ -150,10 +150,13 @@ fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
map_plane_ref(m, base);
map_geo_ref(m, point);
}
PlaneRef::AtAngle { base, axis_origin, axis_dir, .. } => {
PlaneRef::AtAngle { base, axis_origin, axis_dir, line, .. } => {
map_plane_ref(m, base);
*axis_origin = apply_point(m, *axis_origin);
*axis_dir = apply_vector(m, *axis_dir);
if let Some(g) = line {
map_geo_ref(m, g);
}
}
// Named origin planes and construction planes are the component's own.
PlaneRef::Origin { .. } | PlaneRef::Construction { .. } => {}
@@ -227,6 +230,22 @@ impl FeatureKind {
}
}
/// [`Document::component_transform`] with each component's first occurrence found by `first`
/// (an index, where many transforms are wanted at once).
pub(crate) fn transform_through<'a>(component: u64, first: impl Fn(u64) -> Option<&'a Occurrence>) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = first(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
}
impl Document {
/// The first occurrence of a component (its placement for authoring).
pub fn occurrence_of(&self, component: u64) -> Option<&crate::Occurrence> {
@@ -235,17 +254,7 @@ impl Document {
/// World transform of a component's frame (through its first occurrence and its parents').
pub fn component_transform(&self, component: u64) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = self.occurrence_of(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
transform_through(component, |c| self.occurrence_of(c))
}
/// One placement: the component the occurrence `path` from the root leads to (the root for
+20 -2
View File
@@ -114,11 +114,17 @@ fn remap_plane(p: &mut PlaneRef, ids: &BTreeMap<u64, u64>, bodies: &BTreeMap<Str
return Err(crate::DocError::Invalid("clone plane reference depth exceeded".into()));
}
match p {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } | PlaneRef::Flipped { base, .. } => remap_plane(base, ids, bodies, depth + 1)?,
PlaneRef::Offset { base, .. } | PlaneRef::Flipped { base, .. } => remap_plane(base, ids, bodies, depth + 1)?,
PlaneRef::ThroughPoint { base, point } => {
remap_plane(base, ids, bodies, depth + 1)?;
remap_geo(point, ids, bodies, depth + 1)?;
}
PlaneRef::AtAngle { base, line, .. } => {
remap_plane(base, ids, bodies, depth + 1)?;
if let Some(g) = line {
remap_geo(g, ids, bodies, depth + 1)?;
}
}
PlaneRef::Face { name, anchor, .. } => {
if let Some(name) = name {
*name = remap_topology(name, ids, bodies);
@@ -209,6 +215,9 @@ impl crate::Feature {
checked_geo(r, 0)?;
}
}
K::Revolve { axis: crate::AxisRef::Model { geo }, .. }
| K::SurfaceRevolve { axis: crate::AxisRef::Model { geo }, .. }
| K::Pattern { pattern: crate::PatternKind::Circular { axis_ref: Some(geo), .. }, .. } => checked_geo(geo, 0)?,
_ => {}
}
// Feature names are remapped once here; the geometry pass below handles only
@@ -269,6 +278,9 @@ impl crate::Feature {
remap_geo(r, ids, bodies, 0)?;
}
}
K::Revolve { axis: crate::AxisRef::Model { geo }, .. }
| K::SurfaceRevolve { axis: crate::AxisRef::Model { geo }, .. }
| K::Pattern { pattern: crate::PatternKind::Circular { axis_ref: Some(geo), .. }, .. } => remap_geo(geo, ids, bodies, 0)?,
_ => {}
}
Ok(())
@@ -289,7 +301,13 @@ fn checked_plane(p: &PlaneRef, depth: usize) -> crate::Result<()> {
return Err(crate::DocError::Invalid("copy reference nesting exceeds 64".into()));
}
match p {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } | PlaneRef::Flipped { base, .. } => checked_plane(base, depth + 1)?,
PlaneRef::Offset { base, .. } | PlaneRef::Flipped { base, .. } => checked_plane(base, depth + 1)?,
PlaneRef::AtAngle { base, line, .. } => {
checked_plane(base, depth + 1)?;
if let Some(g) = line {
checked_geo(g, depth + 1)?;
}
}
PlaneRef::Midplane { a, b } | PlaneRef::TwoEdges { a, b } => {
checked_geo(a, depth + 1)?;
checked_geo(b, depth + 1)?;
+166
View File
@@ -0,0 +1,166 @@
//! Identification colours, independent of materials and appearances.
use crate::Document;
/// Automatic colours select this palette in creation order; cycling uses the same order.
/// Colors are generated by picking colors on a color wheel, with 0.9 brightness and 0.8 saturation.
/// First pass is 3 colors, 120 huedegrees from each other. Second pass starts with 60 degrees from
/// the first one. Then third and fourth pass fill 60 and 30 degrees gaps between already generated
/// colors of the palette.
pub const PALETTE: [[u8; 3]; 24] = [
[230, 161, 92],
[92, 230, 161],
[161, 92, 230],
[161, 230, 92],
[92, 161, 230],
[230, 92, 161],
[230, 230, 92],
[92, 230, 92],
[92, 230, 230],
[92, 92, 230],
[230, 92, 230],
[230, 92, 92],
[230, 195, 92],
[195, 230, 92],
[126, 230, 92],
[92, 230, 126],
[92, 230, 195],
[92, 195, 230],
[92, 126, 230],
[126, 92, 230],
[195, 92, 230],
[230, 92, 195],
[230, 92, 126],
[230, 126, 92],
];
/// Select a colour in creation order, wrapping at the end of the palette.
pub fn automatic_color(index: u64) -> [u8; 3] {
PALETTE.get((index % PALETTE.len() as u64) as usize).copied().unwrap_or([230, 161, 92])
}
pub fn next_color(color: [u8; 3]) -> [u8; 3] {
let index = PALETTE.iter().position(|c| *c == color).map_or(0, |i| (i + 1) % PALETTE.len());
PALETTE.get(index).copied().unwrap_or([230, 161, 92])
}
impl Document {
pub fn component_identification_color(&self, id: u64) -> [u8; 3] {
self.components.iter().enumerate().find(|(_, c)| c.id == id).map_or_else(
|| automatic_color(0),
|(position, c)| c.identification_color.unwrap_or_else(|| automatic_color(c.identification_color_index.unwrap_or(position as u64))),
)
}
/// Fill older records in stored order and repair stale counters without changing saved indices.
pub(crate) fn normalise_component_color_indices(&mut self) -> crate::Result<()> {
let mut next = self.next_component_color_index;
for component in &self.components {
if let Some(index) = component.identification_color_index {
next = next.max(index.checked_add(1).ok_or_else(|| crate::DocError::Invalid("component colour sequence exhausted".into()))?);
}
}
for component in &mut self.components {
if component.identification_color_index.is_none() {
let following = next.checked_add(1).ok_or_else(|| crate::DocError::Invalid("component colour sequence exhausted".into()))?;
component.identification_color_index = Some(next);
next = following;
}
}
self.next_component_color_index = next;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn old_components_default_to_stable_colors_and_overrides_round_trip() {
let mut doc = Document::default();
let id = doc.add_component(Some("Part"), 0).unwrap();
let automatic = doc.component_identification_color(id);
let mut legacy: serde_json::Value = serde_json::from_str(&doc.to_json()).unwrap();
legacy["components"][0].as_object_mut().unwrap().remove("identification_color");
if let Some(pending) = legacy.get_mut("format_pending").and_then(|v| v.as_array_mut()) {
pending.retain(|name| name != "component-identification-colors");
}
let restored = Document::from_json(&legacy.to_string()).unwrap();
assert_eq!(restored.component_identification_color(id), automatic);
assert_eq!(restored.components[0].identification_color, None);
let override_color = next_color(automatic);
assert_ne!(override_color, automatic);
doc.components[0].identification_color = Some(override_color);
doc.components[0].name = "Renamed".into();
let saved = Document::from_json(&doc.to_json()).unwrap();
assert_eq!(saved.component_identification_color(id), override_color);
assert_eq!(saved.components[0].identification_color, Some(override_color));
}
#[test]
fn creation_order_wraps_and_survives_deletion_reordering_and_reopening() {
let mut doc = Document::default();
let mut ids = Vec::new();
for index in 0..25 {
// IDs also belong to occurrences, so their numeric values are not colour indices.
let id = doc.add_component(None, 0).unwrap();
assert_eq!(doc.component_identification_color(id), PALETTE[index % PALETTE.len()]);
ids.push(id);
}
let retained = ids[1];
doc.components.retain(|c| c.id != ids[0]);
doc.occurrences.retain(|o| o.component != ids[0]);
doc.components.reverse();
let mut restored = Document::from_json(&doc.to_json()).unwrap();
assert_eq!(restored.component_identification_color(retained), PALETTE[1]);
let next = restored.add_component(None, 0).unwrap();
assert_eq!(restored.component_identification_color(next), PALETTE[1]);
assert_eq!(restored.next_component_color_index, 26);
}
#[test]
fn legacy_order_and_overrides_are_preserved_and_stale_counters_repaired() {
let mut doc = Document::default();
let first = doc.add_component(None, 0).unwrap();
let second = doc.add_component(None, first).unwrap();
doc.components[1].identification_color = Some([1, 2, 3]);
let mut json: serde_json::Value = serde_json::from_str(&doc.to_json()).unwrap();
json.as_object_mut().unwrap().remove("next_component_color_index");
for component in json["components"].as_array_mut().unwrap() {
component.as_object_mut().unwrap().remove("identification_color_index");
}
if let Some(pending) = json.get_mut("format_pending").and_then(|v| v.as_array_mut()) {
pending.retain(|name| name != "sequential-component-colors");
}
let mut restored = Document::from_json(&json.to_string()).unwrap();
assert_eq!(restored.components[0].identification_color_index, Some(0));
assert_eq!(restored.components[1].identification_color_index, Some(1));
assert_eq!(restored.component_identification_color(first), PALETTE[0]);
assert_eq!(restored.component_identification_color(second), [1, 2, 3]);
let third = restored.add_component(None, 0).unwrap();
assert_eq!(restored.component_identification_color(third), PALETTE[2]);
restored.next_component_color_index = 0;
let mut repaired = Document::from_json(&restored.to_json()).unwrap();
let fourth = repaired.add_component(None, 0).unwrap();
assert_eq!(repaired.component_identification_color(fourth), PALETTE[3]);
let mut hostile: serde_json::Value = serde_json::from_str(&repaired.to_json()).unwrap();
hostile["components"][0]["identification_color_index"] = u64::MAX.into();
assert!(Document::from_json(&hostile.to_string()).is_err());
}
#[test]
fn cycling_visits_every_palette_color_and_wraps() {
let original = automatic_color(123);
let mut seen = std::collections::BTreeSet::new();
let mut color = original;
for _ in 0..PALETTE.len() {
assert!(seen.insert(color));
color = next_color(color);
}
assert_eq!(color, original);
assert_eq!(automatic_color(0), PALETTE[0]);
assert_eq!(automatic_color(1), PALETTE[1]);
assert_eq!(automatic_color(24), PALETTE[0]);
}
}
+2
View File
@@ -130,6 +130,7 @@ impl Document {
/// A column's current value in the design.
pub fn config_current(&self, c: &Column) -> Option<String> {
match c {
Column::Param(n) if self.text_param(n).is_some() => self.text_param(n).map(|p| p.text.clone()),
Column::Param(n) => self.all_param_exprs().into_iter().find(|(m, _, _)| m == n).map(|(_, e, kind)| {
// Feature literals are stored in base units, unlike newly typed bare
// row cells. Capture their unit explicitly before editing in an inch design.
@@ -262,6 +263,7 @@ impl Document {
}
self.configs.columns.iter().zip(&r.cells).all(|(c, v)| match c {
Column::Suppress(id) => suppression_cell(v).is_ok_and(|on| self.feature(*id).is_some_and(|f| f.suppressed == on)),
Column::Param(name) if self.text_param(name).is_some() => self.text_param(name).is_some_and(|p| p.text == crate::text_params::unquote(v)),
Column::Param(name) => {
let kind = self
.features
+83 -19
View File
@@ -162,6 +162,11 @@ fn face_of<'a>(st: &'a ModelState, at: Vec3, name: &Option<String>) -> Result<(&
/// The edge picked, as a polyline.
fn edge_points(st: &ModelState, at: Vec3, name: &Option<String>) -> Result<Vec<Vec3>> {
edge_pick(st, at, name).map(|(_, _, pts)| pts)
}
/// The edge picked: its body, its index there (as `Body::edges` lists it) and its polyline.
fn edge_pick<'a>(st: &'a ModelState, at: Vec3, name: &Option<String>) -> Result<(&'a ModelBody, usize, Vec<Vec3>)> {
let mut at = at;
if let Some(n) = name {
for b in bodies(st) {
@@ -171,23 +176,33 @@ fn edge_points(st: &ModelState, at: Vec3, name: &Option<String>) -> Result<Vec<V
}
}
}
let mut best: Option<(f64, Vec<Vec3>)> = None;
let mut best: Option<(f64, &ModelBody, usize, Vec<Vec3>)> = None;
for b in bodies(st) {
let tol = (b.body.size() * 1e-3).max(1e-3);
let Ok(edges) = b.body.edges(tol) else { continue };
for e in edges {
for (i, e) in edges.into_iter().enumerate() {
let d = e.points.windows(2).map(|w| at.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
if best.as_ref().is_none_or(|(bd, _)| d < *bd) {
best = Some((d, e.points));
if best.as_ref().is_none_or(|(bd, ..)| d < *bd) {
best = Some((d, b, i, e.points));
}
}
}
match best {
Some((d, pts)) if pts.len() >= 2 && d <= 1e-3 + 1e-2 * pts.iter().fold(0.0, |m: f64, p| m.max(p.len())).max(1.0) => Ok(pts),
Some((d, b, i, pts)) if pts.len() >= 2 && d <= 1e-3 + 1e-2 * pts.iter().fold(0.0, |m: f64, p| m.max(p.len())).max(1.0) => Ok((b, i, pts)),
_ => Err(bad(format!("no edge at {at:?}"))),
}
}
/// The circle a picked edge lies on, from its exact curve (`kernel::edge_circle`) when the drawn
/// edge lies on it, else fitted to the drawn points: (centre, unit normal).
fn edge_circle(b: &ModelBody, index: usize, pts: &[Vec3]) -> Option<(Vec3, Vec3)> {
let exact = kernel::edge_circle(&b.body, index).filter(|c| {
let tol = (b.body.size() * 1e-6).max(1e-9) * 10.0;
pts.iter().all(|p| (p.dist(c.center) - c.radius).abs() <= tol && (*p - c.center).dot(*c.axis).abs() <= tol)
});
exact.map(|c| (c.center, *c.axis)).or_else(|| as_circle(pts).map(|(c, n, _)| (c, n)))
}
/// The most edge names a vertex reference keeps.
const MAX_VERTEX_EDGES: usize = 16;
@@ -306,13 +321,52 @@ pub(crate) fn point(ctx: &Ctx, g: &GeoRef) -> Result<Vec3> {
}
}
/// An edge or face of the model as a line: along a straight edge, through the centre of a
/// circular edge along its normal, or the axis of a cylinder, cone or torus face.
fn model_line(st: &ModelState, g: &GeoRef) -> Result<(Vec3, Vec3)> {
match g {
GeoRef::Edge { at, name } => {
let (b, i, pts) = edge_pick(st, *at, name)?;
as_line(&pts).or_else(|| edge_circle(b, i, &pts)).ok_or_else(|| bad("the edge is neither straight nor circular"))
}
GeoRef::Face { at, name } => {
let (b, fi, _) = face_of(st, *at, name)?;
match kernel::face_surface(&b.body, fi) {
Some(FaceSurface::Cylinder { origin, axis, .. }) => Ok((origin, *axis)),
Some(FaceSurface::Cone { apex, axis, .. }) => Ok((apex, *axis)),
Some(FaceSurface::Torus { center, axis, .. }) => Ok((center, *axis)),
_ => Err(bad("the face is not a cylinder, cone or torus")),
}
}
_ => Err(bad("pick a straight or circular edge, or a cylinder, cone or torus face")),
}
}
/// The axis a feature turns about when an edge or face of the model is picked for it (a revolve,
/// a circular pattern, a plane at an angle), as [`model_line`] finds it. The direction is
/// oriented the same way however the edge or surface runs (its largest component positive), so
/// the feature keeps its turn when an edit rebuilds the edge reversed.
pub fn model_axis(st: &ModelState, g: &GeoRef) -> Result<(Vec3, Vec3)> {
let (o, d) = model_line(st, g)?;
let d = d.normalized().filter(|_| o.is_finite()).ok_or_else(|| bad("the axis has no direction"))?;
Ok((o, canonical_dir(d)))
}
/// `d` or `-d`, whichever has its largest component (the first of equal ones) positive: how
/// [`model_axis`] orients an axis, for a preview of one to point the same way.
pub fn canonical_dir(d: Vec3) -> Vec3 {
let c = [d.x, d.y, d.z];
let big = c.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
match c.iter().find(|v| v.abs() >= big - 1e-9) {
Some(v) if *v < 0.0 => -d,
_ => d,
}
}
/// A reference as a line (origin, unit direction).
pub(crate) fn line(ctx: &Ctx, g: &GeoRef) -> Result<(Vec3, Vec3)> {
match g {
GeoRef::Edge { at, name } => {
let pts = edge_points(ctx.st, *at, name)?;
as_line(&pts).or_else(|| as_circle(&pts).map(|(c, n, _)| (c, n))).ok_or_else(|| bad("the edge is neither straight nor circular"))
}
GeoRef::Edge { .. } | GeoRef::Face { .. } => model_line(ctx.st, g),
GeoRef::SketchCurve { sketch, curve } => {
let pts = sketch_curve_points(ctx, *sketch, curve)?;
as_line(&pts).ok_or_else(|| bad(format!("sketch curve `{curve}` is not a line")))
@@ -330,15 +384,6 @@ pub(crate) fn line(ctx: &Ctx, g: &GeoRef) -> Result<(Vec3, Vec3)> {
ConstructGeom::Axis { origin, dir } => Ok((*origin, *dir)),
_ => Err(bad(format!("`{name}` is not an axis"))),
},
GeoRef::Face { at, name } => {
let (b, fi, _) = face_of(ctx.st, *at, name)?;
match kernel::face_surface(&b.body, fi) {
Some(FaceSurface::Cylinder { origin, axis, .. }) => Ok((origin, *axis)),
Some(FaceSurface::Cone { apex, axis, .. }) => Ok((apex, *axis)),
Some(FaceSurface::Torus { center, axis, .. }) => Ok((center, *axis)),
_ => Err(bad("the face is not a cylinder, cone or torus")),
}
}
_ => Err(bad("pick an edge, line or axis")),
}
}
@@ -602,6 +647,20 @@ pub(crate) fn resolve_geo_plane(ctx: &Ctx, p: &PlaneRef) -> Result<Plane> {
}
impl GeoRef {
/// Where a picked edge or face is now, found the way evaluation finds it (by its persistent
/// name, else at the picked point): a point on it. `None` for other references and for an
/// edge or face that is gone.
pub fn picked_point(&self, st: &ModelState) -> Option<Vec3> {
match self {
GeoRef::Edge { at, name } => {
let on = name.as_deref().and_then(|n| bodies(st).find_map(|b| crate::naming::find_edge(b, n, *at).ok())).map_or(*at, |(q, _)| q);
edge_pick(st, on, &None).ok().map(|_| on)
}
GeoRef::Face { at, name } => face_of(st, *at, name).ok().map(|(_, _, p)| p),
_ => None,
}
}
/// Map the reference's world points (component frames).
pub fn map_points(&mut self, pt: &mut dyn FnMut(&mut Vec3), plane: &mut dyn FnMut(&mut PlaneRef)) {
match self {
@@ -710,7 +769,12 @@ impl PlaneRef {
refs.push(point.as_mut());
refs
}
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } | PlaneRef::Flipped { base, .. } => base.refs_mut(),
PlaneRef::AtAngle { base, line, .. } => {
let mut v = base.refs_mut();
v.extend(line.as_deref_mut());
v
}
PlaneRef::Offset { base, .. } | PlaneRef::Flipped { base, .. } => base.refs_mut(),
_ => Vec::new(),
}
}
+496
View File
@@ -5,6 +5,10 @@
//! These functions format values, not validate them: non-finite inputs retain Rust's `NaN`,
//! `inf` and `-inf` text. Callers that require finite measurements must validate them first.
use serde::{Deserialize, Serialize};
use crate::{DocError, Result, expr};
/// Units per millimetre and the unit's name. Unknown names use millimetres.
pub fn unit_scale(units: &str) -> (f64, &'static str) {
match units {
@@ -39,6 +43,142 @@ pub fn format_length(mm: f64, units: &str, decimals: u8) -> String {
format!("{} {u}", length_number(mm, units, decimals))
}
/// The decimal separator of a number, independent of UI language.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecimalMarker {
Point,
Comma,
}
/// How an exact tie in the prepared decimal digits is rounded.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Ties {
HalfEven,
HalfAwayFromZero,
}
/// Number presentation for non-UI consumers. This does not change the legacy length helpers.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct NumberFormat {
pub marker: DecimalMarker,
pub groups: bool,
pub leading_zero: bool,
pub trailing_zeros: bool,
pub ties: Ties,
}
impl Default for NumberFormat {
fn default() -> Self {
Self { marker: DecimalMarker::Point, groups: false, leading_zero: true, trailing_zeros: true, ties: Ties::HalfEven }
}
}
/// Formatted text without a unit suffix, and the rounded number in the display unit.
#[derive(Clone, Debug, PartialEq)]
pub struct Shown {
pub text: String,
pub value: f64,
}
/// Format an internal value (millimetres or radians) in a strictly validated display unit.
pub fn format_value(value: f64, kind: expr::Kind, unit: &str, decimals: u8, f: &NumberFormat) -> Result<Shown> {
let (unit_kind, scale) = expr::unit_info(unit).ok_or_else(|| DocError::Invalid("unknown number display unit".into()))?;
if unit_kind != kind {
return Err(DocError::Invalid("number display unit has a different kind".into()));
}
format_display(value / scale, scale, decimals, f)
}
/// Format a unit-less number with the same validation and decimal rounding policy.
pub fn format_number(x: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
format_display(x, 1.0, decimals, f)
}
fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
// The unit table supplies a positive finite scale. Its decimal step must be at most
// 1e-9 internal units; the precision floor can make the effective tie window narrower.
let unit_resolution = (scale.log10() + 9.0).ceil().max(0.0) as usize;
let resolution = unit_resolution.max(usize::from(decimals) + 3);
let (rounded, value) = round_decimal(x, decimals, resolution, f.ties)?;
let (integer, fraction) = rounded.split_once('.').unwrap_or((&rounded, ""));
let fraction = if f.trailing_zeros { fraction } else { fraction.trim_end_matches('0') };
let integer = if !f.leading_zero && integer == "0" && !fraction.is_empty() { "" } else { integer };
let mut text = String::new();
if value.is_sign_negative() {
text.push('−');
}
grouped_digits(&mut text, integer, true, f.groups);
if !fraction.is_empty() {
text.push(match f.marker {
DecimalMarker::Point => '.',
DecimalMarker::Comma => ',',
});
grouped_digits(&mut text, fraction, false, f.groups);
}
Ok(Shown { text, value })
}
/// Prepare a bounded magnitude, then round decimal digits, never a binary tie comparison.
fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
if decimals > 8 {
return Err(DocError::Invalid("number precision must be at most 8 decimal places".into()));
}
if !x.is_finite() || x.abs() > 1e12 {
return Err(DocError::Invalid("display value must be finite with magnitude at most 1e12".into()));
}
let prepared = format!("{:.*}", resolution, x.abs());
let (integer, fraction) = prepared.split_once('.').unwrap_or((&prepared, ""));
let places = usize::from(decimals);
let mut digits: Vec<u8> = integer.bytes().chain(fraction.bytes().take(places)).collect();
let discarded = fraction.as_bytes().get(places..).unwrap_or_default();
let up = match discarded.first().copied() {
Some(b'6'..=b'9') => true,
Some(b'5') => {
discarded.get(1..).unwrap_or_default().iter().any(|d| *d != b'0')
|| match ties {
Ties::HalfEven => digits.last().is_some_and(|d| !(d - b'0').is_multiple_of(2)),
Ties::HalfAwayFromZero => true,
}
}
_ => false,
};
if up {
let mut carry = true;
for d in digits.iter_mut().rev() {
if *d == b'9' {
*d = b'0';
} else {
*d += 1;
carry = false;
break;
}
}
if carry {
digits.insert(0, b'1');
}
}
if places > 0 {
let point = digits.len().checked_sub(places).ok_or_else(|| DocError::Invalid("invalid prepared number".into()))?;
digits.insert(point, b'.');
}
let rounded = String::from_utf8(digits).map_err(|_| DocError::Invalid("invalid prepared digits".into()))?;
let magnitude = rounded.parse::<f64>().map_err(|_| DocError::Invalid("invalid rounded number".into()))?;
let negative = x.is_sign_negative() && magnitude != 0.0;
Ok((rounded, if negative { -magnitude } else { magnitude }))
}
fn grouped_digits(text: &mut String, digits: &str, integer: bool, groups: bool) {
for (i, digit) in digits.chars().enumerate() {
if groups && digits.len() >= 5 && i > 0 && if integer { (digits.len() - i).is_multiple_of(3) } else { i.is_multiple_of(3) } {
text.push('\u{202f}');
}
text.push(digit);
}
}
#[cfg(test)]
mod tests {
use super::{format_length, length_number, length_precision, unit_scale};
@@ -121,3 +261,359 @@ mod tests {
assert_eq!(format_length(f64::NEG_INFINITY, "cm", 2), "-inf cm");
}
}
#[cfg(test)]
mod number_tests {
use super::*;
use expr::Kind::{Angle, Length, Unitless};
fn pair(x: f64, dp: u8, even: &str, away: &str) {
for (ties, expected) in [(Ties::HalfEven, even), (Ties::HalfAwayFromZero, away)] {
let shown = format_number(x, dp, &NumberFormat { ties, ..NumberFormat::default() }).unwrap();
assert_eq!(shown.text, expected, "x={x:?}, dp={dp}, ties={ties:?}");
let parsed: f64 = expected.replace('−', "-").parse().unwrap();
assert_eq!(shown.value, parsed);
}
}
#[test]
fn number_exact_binary_ties() {
for (x, dp, even, away) in [
(0.125, 2, "0.12", "0.13"),
(0.375, 2, "0.38", "0.38"),
(12.5, 0, "12", "13"),
(-12.5, 0, "−12", "−13"),
(13.5, 0, "14", "14"),
(-13.5, 0, "−14", "−14"),
] {
pair(x, dp, even, away);
}
}
#[test]
fn number_decimal_ties() {
for (x, even, away) in [(2.675, "2.68", "2.68"), (2.665, "2.66", "2.67"), (1.005, "1.00", "1.01"), (0.285, "0.28", "0.29")] {
pair(x, 2, even, away);
}
// The existing UI policy deliberately remains different.
assert_eq!(length_number(2.665, "mm", 2), "2.67");
assert_eq!(length_number(2.675, "mm", 2), "2.67");
assert_eq!(length_number(1.005, "mm", 2), "1.00");
assert_eq!(length_number(0.285, "mm", 2), "0.28");
}
#[test]
fn number_tie_zone_edges_and_precision_floor() {
for (x, expected) in [(61.49999999999999, "62"), (61.4999999996, "62"), (61.4999999994, "61"), (61.4999997, "61"), (61.4996, "61")] {
pair(x, 0, expected, expected);
}
pair(1.0000000049, 8, "1.00000000", "1.00000000");
pair(2.4501, 1, "2.5", "2.5");
pair(2.5499, 1, "2.5", "2.5");
}
#[test]
fn number_carries() {
pair(9.995, 2, "10.00", "10.00");
pair(-9.995, 2, "−10.00", "−10.00");
pair(99.5, 0, "100", "100");
pair(0.999999995, 8, "1.00000000", "1.00000000");
}
#[test]
fn number_units_and_aliases() {
let f = NumberFormat::default();
for (x, kind, unit, dp, expected) in [
(25.4, Length, "in", 3, "1.000"),
(25.4 * (1.0 + 1e-12), Length, "in", 3, "1.000"),
(25.4, Length, "in", 0, "1"),
(12.5, Length, "cm", 2, "1.25"),
(3810.0, Length, "ft", 1, "12.5"),
(std::f64::consts::PI / 6.0, Angle, "deg", 1, "30.0"),
(1.0, Angle, "deg", 3, "57.296"),
(1.0, Angle, "rad", 3, "1.000"),
(0.001, Length, "um", 3, "1.000"),
(0.001, Length, "µm", 3, "1.000"),
(1e-6, Length, "nm", 3, "1.000"),
(0.0254, Length, "mil", 3, "1.000"),
(914.4, Length, "yd", 3, "1.000"),
(1e6, Length, "km", 3, "1.000"),
(25.4, Length, " inch ", 3, "1.000"),
(std::f64::consts::PI, Angle, "°", 1, "180.0"),
(std::f64::consts::PI, Angle, "degrees", 1, "180.0"),
(1.25, Unitless, "", 2, "1.25"),
(1.25, Unitless, "none", 2, "1.25"),
(1.25, Unitless, "unitless", 2, "1.25"),
] {
let shown = format_value(x, kind, unit, dp, &f).unwrap();
assert_eq!(shown.text, expected, "{x:?} {kind:?} in {unit:?}");
assert_eq!(shown.value, expected.parse::<f64>().unwrap());
}
for (kind, unit) in [(Length, "deg"), (Angle, "mm"), (Length, ""), (Length, "parsec"), (Unitless, "mm"), (Length, "IN")] {
assert!(matches!(format_value(1.0, kind, unit, 2, &f), Err(DocError::Invalid(_))), "{kind:?} {unit:?}");
}
}
#[test]
fn number_unit_resolution_edges() {
let f = NumberFormat { ties: Ties::HalfAwayFromZero, ..NumberFormat::default() };
for (kind, unit, resolution) in [
(Length, "mm", 9),
(Length, "cm", 10),
(Length, "in", 11),
(Length, "m", 12),
(Length, "ft", 12),
(Angle, "deg", 8),
(Unitless, "", 9),
(Length, "nm", 3),
(Length, "km", 15),
] {
let scale = expr::unit_info(unit).unwrap().1;
let quantum = 10.0_f64.powi(-resolution);
for (distance, expected) in [(0.4, "1"), (0.6, "0")] {
let internal = (0.5 - distance * quantum) * scale;
assert_eq!(
format_value(internal, kind, unit, 0, &f).unwrap().text,
expected,
"{unit:?}, resolution={resolution}, distance={distance}"
);
}
}
}
#[test]
fn number_zero_sign_caps_and_shown_value() {
for x in [-0.0, -0.004, f64::from_bits(1)] {
let shown = format_number(x, 2, &NumberFormat::default()).unwrap();
assert_eq!(shown.text, "0.00");
assert_eq!(shown.value.to_bits(), 0.0_f64.to_bits());
}
pair(-1.5, 1, "−1.5", "−1.5");
pair(12.5, 8, "12.50000000", "12.50000000");
let f = NumberFormat::default();
for dp in [9, 255] {
assert!(matches!(format_number(12.5, dp, &f), Err(DocError::Invalid(_))));
assert!(matches!(format_value(25.4, Length, "in", dp, &f), Err(DocError::Invalid(_))));
}
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e13, -1e13, 1e12_f64.next_up(), -1e12_f64.next_up()] {
assert!(matches!(format_number(x, 2, &f), Err(DocError::Invalid(_))), "{x:?}");
assert!(matches!(format_value(x, Length, "mm", 2, &f), Err(DocError::Invalid(_))), "{x:?}");
}
for x in [-1e12, 1e12] {
assert_eq!(format_number(x, 8, &f).unwrap().value, x);
}
assert_eq!(format_value(1e15, Length, "m", 8, &f).unwrap().value, 1e12, "cap is in display units");
assert_eq!(format_number(2.665, 2, &f).unwrap().value, 2.66);
assert_eq!(format_value(25.4, Length, "in", 3, &f).unwrap().value, 1.0);
}
#[test]
fn number_format_options_and_group_boundaries() {
let base = NumberFormat::default();
let comma = NumberFormat { marker: DecimalMarker::Comma, ..base };
assert_eq!(format_number(2.665, 2, &comma).unwrap().text, "2,66");
let no_leading = NumberFormat { leading_zero: false, ..base };
assert_eq!(format_number(0.5, 3, &no_leading).unwrap().text, ".500");
let trim = NumberFormat { trailing_zeros: false, ..base };
assert_eq!(format_number(25.0, 2, &trim).unwrap().text, "25");
assert_eq!(format_number(25.5, 2, &trim).unwrap().text, "25.5");
assert_eq!(format_number(0.5, 3, &NumberFormat { leading_zero: false, ..trim }).unwrap().text, ".5");
let groups = NumberFormat { groups: true, ..base };
for (x, dp, expected) in [
(12345.6789, 4, "12\u{202f}345.6789"),
(1.23456, 5, "1.234\u{202f}56"),
(1234.5678, 4, "1234.5678"),
(123456.0, 0, "123\u{202f}456"),
(1234.0, 0, "1234"),
(12345.0, 0, "12\u{202f}345"),
(1.23456789, 8, "1.234\u{202f}567\u{202f}89"),
(-12345.6789, 4, "−12\u{202f}345.6789"),
(9999.9, 0, "10\u{202f}000"),
] {
assert_eq!(format_number(x, dp, &groups).unwrap().text, expected, "{x:?}, dp={dp}");
}
assert_eq!(format_number(12345.6789, 4, &NumberFormat { groups: true, ..comma }).unwrap().text, "12\u{202f}345,6789");
assert_eq!(format_number(1.23450, 5, &NumberFormat { groups: true, ..trim }).unwrap().text, "1.2345", "group printed digits after trimming");
}
#[test]
fn number_provisional_leading_zero_for_rounded_zero() {
// Provisional #5290 assumption: a fractional zero also omits the integer zero.
let f = NumberFormat { leading_zero: false, ..NumberFormat::default() };
for x in [0.0, -0.0, -0.004] {
assert_eq!(format_number(x, 2, &f).unwrap().text, ".00");
assert_eq!(format_number(x, 0, &f).unwrap().text, "0");
assert_eq!(format_number(x, 2, &NumberFormat { trailing_zeros: false, ..f }).unwrap().text, "0");
}
assert_eq!(format_number(-0.5, 2, &f).unwrap().text, "−.50");
assert_eq!(format_number(0.0, 2, &NumberFormat { marker: DecimalMarker::Comma, ..f }).unwrap().text, ",00");
}
#[test]
fn number_provisional_serde_convention_and_strict_fields() {
// Provisional #5290 assumption, matching expr::Kind and other doc enums.
let f: NumberFormat = serde_json::from_str("{}").unwrap();
assert_eq!(f, NumberFormat::default());
assert_eq!(
serde_json::to_value(f).unwrap(),
serde_json::json!({
"marker": "point", "groups": false, "leading_zero": true,
"trailing_zeros": true, "ties": "half_even"
})
);
let f: NumberFormat = serde_json::from_value(serde_json::json!({"marker":"comma", "ties":"half_away_from_zero"})).unwrap();
assert_eq!(f.marker, DecimalMarker::Comma);
assert_eq!(f.ties, Ties::HalfAwayFromZero);
assert!(f.leading_zero && f.trailing_zeros && !f.groups);
assert_eq!(serde_json::from_value::<NumberFormat>(serde_json::to_value(f).unwrap()).unwrap(), f);
for bad in [serde_json::json!({"locale":"en"}), serde_json::json!({"marker":"Point"}), serde_json::json!({"ties":"unknown"})] {
assert!(serde_json::from_value::<NumberFormat>(bad).is_err());
}
}
fn random(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
/// Compare the printed decimal exactly with the input's binary fraction. A second f64
/// subtraction can exceed the bound solely because parsing the correct text rounds again.
fn decimal_within_bound(x: f64, text: &str, dp: u8, resolution: usize) -> bool {
let unsigned = text.trim_start_matches('-');
let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
let coefficient: u128 = format!("{whole}{fraction}").parse().unwrap();
let coefficient = coefficient * 10_u128.pow(u32::from(dp) - fraction.len() as u32);
let extra = 10_u128.pow((resolution - usize::from(dp)) as u32);
// Multiply |text - input| <= (10^-dp + 10^-resolution)/2 by 2*10^resolution.
let center = 2 * coefficient * extra;
let radius = extra + 1;
let bits = x.abs().to_bits();
let exponent = ((bits >> 52) & 0x7ff) as u32;
let mantissa = u128::from(bits & ((1_u64 << 52) - 1));
let (mantissa, shift) = if exponent == 0 { (mantissa, 1074) } else { (mantissa | (1_u128 << 52), 1075 - exponent) };
let input = 2 * mantissa * 10_u128.pow(resolution as u32);
// Input is below 2^104 at the supported resolutions. A shifted bound above u128's
// range is therefore certainly above input; saturation preserves the comparison.
let shifted = |n: u128| {
if n == 0 {
0
} else if shift >= 128 || n > (u128::MAX >> shift) {
u128::MAX
} else {
n << shift
}
};
shifted(center.saturating_sub(radius)) <= input && input <= shifted(center + radius)
}
#[test]
fn number_large_magnitude_decimal_error_bound() {
let x = 34947628153.461784;
let shown = format_number(x, 5, &NumberFormat::default()).unwrap();
assert_eq!(shown.text, "34947628153.46178");
assert!(decimal_within_bound(x, &shown.text, 5, 9));
assert!(!decimal_within_bound(x, "34947628153.46177", 5, 9));
// Another exact binary tie whose rounded decimal has an inexact f64 representation.
let x = 100000000000.25;
let shown = format_number(x, 1, &NumberFormat::default()).unwrap();
assert_eq!(shown.text, "100000000000.2");
assert!(decimal_within_bound(x, &shown.text, 1, 9));
}
#[test]
fn number_seeded_100k_properties() {
// Reference resolutions derived by hand from the unit table, not from the formatter.
let units = [
("mm", 9),
("cm", 10),
("m", 12),
("in", 11),
("ft", 12),
("yd", 12),
("km", 15),
("um", 6),
("µm", 6),
("nm", 3),
("mil", 8),
("inch", 11),
("deg", 8),
("rad", 9),
("°", 8),
("degree", 8),
("degrees", 8),
("", 9),
("none", 9),
("unitless", 9),
(" in ", 11),
("parsec", 9),
("IN", 9),
("\0", 9),
("../../mm", 9),
("🦀", 9),
];
let mut seed = 0x5290_a735_19d2_8bc1;
let (mut accepted, mut rejected, mut ordered) = (0, 0, 0);
for case in 0..100_000 {
let x = f64::from_bits(random(&mut seed));
let dp = (random(&mut seed) % 13) as u8;
let bits = random(&mut seed);
let f = NumberFormat {
marker: if bits & 1 == 0 { DecimalMarker::Point } else { DecimalMarker::Comma },
groups: bits & 2 != 0,
leading_zero: bits & 4 != 0,
trailing_zeros: bits & 8 != 0,
ties: if bits & 16 == 0 { Ties::HalfEven } else { Ties::HalfAwayFromZero },
};
let kind = match random(&mut seed) % 3 {
0 => Length,
1 => Angle,
_ => Unitless,
};
let (unit, base_resolution) = units[(random(&mut seed) % units.len() as u64) as usize];
let bare = case % 2 == 0;
let info = if bare { Some((Unitless, 1.0)) } else { expr::unit_info(unit).filter(|(k, _)| *k == kind) };
let call = |v| if bare { format_number(v, dp, &f) } else { format_value(v, kind, unit, dp, &f) };
let result = call(x);
let display = info.map(|(_, scale)| x / scale);
let valid = dp <= 8 && display.is_some_and(|v| v.is_finite() && v.abs() <= 1e12);
if !valid {
rejected += 1;
assert!(matches!(result, Err(DocError::Invalid(_))), "case {case}: {x:?}, dp={dp}, {kind:?}, {unit:?}: {result:?}");
continue;
}
accepted += 1;
let shown = result.unwrap();
let canonical = shown.text.replace(',', ".").replace('\u{202f}', "").replace('−', "-");
let parsed: f64 = canonical.parse().unwrap();
assert_eq!(shown.value.to_bits(), parsed.to_bits(), "case {case}: {shown:?}");
assert!(!shown.text.contains('-'));
if parsed == 0.0 {
assert!(!shown.value.is_sign_negative() && !shown.text.starts_with('−'));
}
let step = 10.0_f64.powi(-i32::from(dp));
let resolution = if bare { 9 } else { base_resolution }.max(usize::from(dp) + 3);
assert!(
decimal_within_bound(display.unwrap(), &canonical, dp, resolution),
"case {case}: input={x:?}, dp={dp}, unit={unit:?}, {shown:?}"
);
if parsed != 0.0 {
assert_eq!(parsed.is_sign_negative(), x.is_sign_negative());
}
// Shown.value already has display units; converting it again would be wrong.
assert_eq!(format_number(shown.value, dp, &f).unwrap().text, shown.text, "idempotence case {case}");
// Fixed settings/units; move by >=2 display steps and several input ULPs.
// Unlike next_up alone, this requires actual output increases, not just equal zeros.
let scale = info.unwrap().1;
let delta = (2.0 * step * scale).max(x.abs() * f64::EPSILON * 4.0);
let y = x + delta;
if let Ok(next) = call(y) {
ordered += 1;
assert!(y > x && next.value > shown.value, "monotonicity case {case}: {x:?}->{y:?}, dp={dp}, unit={unit:?}: {shown:?}->{next:?}");
}
}
assert!(accepted > 10_000 && rejected > 10_000 && ordered > 10_000, "coverage: accepted={accepted}, rejected={rejected}, ordered={ordered}");
eprintln!("seed=0x5290a73519d28bc1 cases=100000 accepted={accepted} rejected={rejected} strictly_ordered={ordered}");
}
}
+226 -29
View File
@@ -15,6 +15,21 @@ pub const MAX_PARAMS: usize = 10_000;
/// numbers, and far from overflowing the next id.
pub const MAX_FEATURE_ID: u64 = 1 << 52;
/// Screw direction independent of the cylinder's placement and the picked end.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, solidcraft_derive::Walk)]
#[serde(rename_all = "lowercase")]
pub enum ThreadHandedness {
#[default]
Right,
Left,
}
impl ThreadHandedness {
pub fn is_right(&self) -> bool {
*self == Self::Right
}
}
/// A named parameter. User parameters are created by the user; model parameters (`d1`, `d2`…)
/// are created by dimensions and features.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -57,8 +72,17 @@ pub enum PlaneRef {
/// Another plane moved along its normal until it goes through a point (a vertex, sketch
/// point or construction point), so it follows the point.
ThroughPoint { base: Box<PlaneRef>, point: Box<GeoRef> },
/// Another plane rotated about a line (in world coordinates) by an angle expression.
AtAngle { base: Box<PlaneRef>, axis_origin: Vec3, axis_dir: Vec3, angle: Expr },
/// Another plane rotated about a line (in world coordinates) by an angle expression. With
/// `line`, an edge or round face of the model ([`crate::construct::model_axis`]), the line is
/// found there on each evaluation and `axis_origin`/`axis_dir` are where it was when picked.
AtAngle {
base: Box<PlaneRef>,
axis_origin: Vec3,
axis_dir: Vec3,
angle: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
line: Option<Box<GeoRef>>,
},
/// A construction plane feature (by name).
Construction { name: FeatureName },
/// `base` seen from its other side: the normal reversed by a half turn about the base's
@@ -98,13 +122,36 @@ pub enum PlaneRef {
/// Complete source curves of an outer boundary and each of its holes. Unlike a
/// span multiset, this identity is independent of adaptive spline subdivision.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
///
/// A loop is a set of curves: `outer` is kept sorted, and `holes` sorted by their own ids, as
/// the sketch's profiles produce them. Any start and either direction of the same loop is
/// accepted and made canonical on load, so a design written by hand or by another writer still
/// opens (#6499).
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct WholeProfileBoundary {
pub outer: Vec<String>,
pub holes: Vec<Vec<String>>,
}
impl<'de> Deserialize<'de> for WholeProfileBoundary {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
/// The written form, before the loop order is dropped.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Written {
outer: Vec<String>,
holes: Vec<Vec<String>>,
}
let Written { mut outer, mut holes } = Written::deserialize(deserializer)?;
outer.sort();
for hole in &mut holes {
hole.sort();
}
holes.sort();
Ok(Self { outer, holes })
}
}
/// Which closed profiles of a sketch a feature uses.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
@@ -371,6 +418,9 @@ pub enum AxisRef {
World { axis: String },
/// Any line in the sketch plane, in world coordinates.
Line { origin: Position, dir: Dir },
/// An edge or round face of the model (a straight or circular edge, a cylinder, cone or
/// torus face), found again on each evaluation: [`crate::construct::model_axis`].
Model { geo: GeoRef },
/// A construction axis feature, by name, where it is when the revolve is evaluated.
Construction { name: FeatureName },
}
@@ -729,6 +779,9 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Thread depth")]
thread_depth: Option<Expr>,
/// Screw direction of the cosmetic thread; older holes are right-handed.
#[serde(default, skip_serializing_if = "ThreadHandedness::is_right")]
handedness: ThreadHandedness,
/// To Object: drill down to the face (or point) here instead of `depth`.
#[serde(default, skip_serializing_if = "Option::is_none")]
to: Option<FacePick>,
@@ -751,6 +804,8 @@ pub enum FeatureKind {
length: Option<Expr>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
modeled: bool,
#[serde(default, skip_serializing_if = "ThreadHandedness::is_right")]
handedness: ThreadHandedness,
},
/// Ruled loft through profiles of several sketches, in order.
Loft {
@@ -1086,7 +1141,7 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Bend radius")]
radius: Option<Expr>,
/// inside | outside | middle.
/// inside | outside | middle | material_outside | tangent (`sheet::SheetBody::add_flanges_each`).
#[serde(default = "inside_str")]
position: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
@@ -1102,6 +1157,11 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Corner relief")]
corner_relief: Option<Expr>,
/// Where the heights are measured from: `inner` (the inner virtual sharp) or `tangent`
/// (past 90° the line square to the flange touching the bend's outside); none is the outer
/// virtual sharp (`sheet::HeightFrom`).
#[serde(default, skip_serializing_if = "Option::is_none")]
height_from: Option<String>,
},
/// Flat hems on sheet edges.
SheetHem {
@@ -1268,6 +1328,14 @@ pub enum FeatureKind {
/// The catch's height along the arm.
#[walk(length = "Catch length")]
catch_length: Expr,
/// The base extends this far below the face (none: the arm starts at the face).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[walk(length = "Base height")]
base_height: Option<Expr>,
/// Make the snap fit its own body instead of joining the target, so it can be
/// patterned or moved on its own.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
new_body: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<BodyName>,
},
@@ -1952,6 +2020,12 @@ pub struct SketchPoints {
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Component {
pub id: u64,
/// Optional identification colour, separate from normal appearances.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identification_color: Option<[u8; 3]>,
/// Creation sequence used to select an automatic identification colour.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identification_color_index: Option<u64>,
pub name: String,
/// Parent component (0 = the root).
#[serde(default)]
@@ -2110,6 +2184,11 @@ pub enum PatternKind {
alignment: solidcraft_sketch::PatternAlignment,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
reversed: bool,
/// The edge or round face of the model the copies turn about, found again on each
/// evaluation ([`crate::construct::model_axis`]); `origin` and `axis` are where it was
/// when picked.
#[serde(default, skip_serializing_if = "Option::is_none")]
axis_ref: Option<GeoRef>,
/// 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")]
@@ -2445,6 +2524,33 @@ pub struct Feature {
pub kind: FeatureKind,
}
impl Feature {
/// Where in `body_names` the names of the first `count` bodies the feature builds are kept,
/// in the order it builds them. The same index for most features. An Import keeps its names
/// by the file's body index, and builds no body for the file bodies it skips, so its built
/// bodies pass their slots by (#2175).
pub fn body_name_slots(&self, count: usize) -> Vec<usize> {
let skip: std::collections::HashSet<u64> = match &self.kind {
FeatureKind::Import { skip, .. } => skip.iter().copied().collect(),
_ => Default::default(),
};
(0..count.saturating_add(skip.len())).filter(|k| !skip.contains(&(*k as u64))).take(count).collect()
}
/// Keep `made`, the names the feature's bodies have now (in the order it builds them), as
/// its stored names, so they stay as they are whatever comes and goes before it. An Import's
/// skipped file bodies keep the names stored for them.
pub fn pin_body_names(&mut self, made: &[String]) {
let slots = self.body_name_slots(made.len());
self.body_names.resize(slots.last().map_or(0, |s| s + 1), String::new());
for (slot, name) in slots.iter().zip(made) {
if let Some(stored) = self.body_names.get_mut(*slot) {
stored.clone_from(name);
}
}
}
}
fn inside_str() -> String {
"inside".into()
}
@@ -2494,6 +2600,11 @@ pub struct DesignLink {
pub feature: u64,
}
/// A name the design's counter hands out (`Body1`).
pub fn is_counted_body_name(name: &str) -> bool {
name.strip_prefix("Body").is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) && n.parse::<usize>().is_ok())
}
/// The document.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Document {
@@ -2513,6 +2624,9 @@ pub struct Document {
#[serde(default, skip_serializing_if = "crate::Metadata::is_empty")]
pub metadata: crate::Metadata,
pub params: Vec<Param>,
/// Named texts the words of sketch texts can be built from ([`crate::text_params`]).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub text_params: Vec<crate::text_params::TextParam>,
pub features: Vec<Feature>,
/// Timeline marker: features at or after this index are rolled back (not evaluated).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2528,6 +2642,9 @@ pub struct Document {
/// Components below the root, as a tree (by parent).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<Component>,
/// Next automatic colour sequence number; deletions never rewind it.
#[serde(default)]
pub next_component_color_index: u64,
/// Placements of components in their parents (each non-root component has at least one).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub occurrences: Vec<crate::Occurrence>,
@@ -2786,11 +2903,13 @@ impl Document {
units: default_units(),
metadata: Default::default(),
params: Vec::new(),
text_params: Vec::new(),
features: Vec::new(),
marker: None,
next_id: 1,
reserved_body_names: Default::default(),
components: Vec::new(),
next_component_color_index: 0,
occurrences: Vec::new(),
assembly: Default::default(),
sheet: Default::default(),
@@ -2815,19 +2934,25 @@ 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
/// features: materials, appearances, plastic rules, 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).
/// again hands the freed name to the next body (#4957). Every counted name is reserved as
/// soon as a body has it (`Session::pin_body_names`), so this only matters for a design
/// that has not been through a command since then. `new` may be a reserved name: a rename
/// is deliberate, and the body then has what the design keeps under it.
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() {
if is_counted_body_name(old) {
self.reserved_body_names.insert(old.to_string());
}
if let Some(m) = self.materials.remove(old) {
self.materials.insert(new.to_string(), m);
}
if let Some(rule) = self.plastic.assigned.remove(old) {
self.plastic.assigned.insert(new.to_string(), rule);
}
if let Some(c) = self.appearances.bodies.remove(old) {
self.appearances.bodies.insert(new.to_string(), c);
}
@@ -2880,8 +3005,10 @@ impl Document {
continue;
}
let now = made(a.id);
// The stored slot of each body built now (an Import's pass its skipped bodies by).
let at: std::collections::HashMap<usize, &String> = b.body_name_slots(now.len()).into_iter().zip(&now).collect();
for (k, name) in b.body_names.iter().enumerate() {
let current = now.get(k);
let current = at.get(&k).copied();
// Slots the rename left alone, and the ones it set to the names they have now.
if a.body_names.get(k) == Some(name) || name.trim().is_empty() || current == Some(name) {
continue;
@@ -2930,21 +3057,30 @@ impl Document {
*self == normalized
}
/// Only components' part properties (metadata and free properties) changed: nothing to
/// Only part properties changed (the fields `with_part_properties_of` names): nothing to
/// evaluate. The same whole-Document proof as [`Document::differs_only_in_render`].
pub fn differs_only_in_part_properties(&self, previous: &Document) -> bool {
if self.components.len() != previous.components.len()
|| self.components.iter().zip(&previous.components).any(|(a, b)| a.id != b.id)
|| self.components == previous.components
{
return false;
previous.with_part_properties_of(self).is_some_and(|normalized| normalized == *self)
}
/// This design with `other`'s part properties, or `None` when they are this design's
/// already (then nothing is copied). The part properties are the fields that change no
/// geometry: the design's metadata, and each component's metadata and free properties. A
/// field that joins them is compared and copied here, and nowhere else.
fn with_part_properties_of(&self, other: &Document) -> Option<Document> {
let same = self.metadata == other.metadata
&& self.components.iter().zip(&other.components).all(|(c, o)| c.metadata == o.metadata && c.properties == o.properties);
if same {
return None;
}
let mut normalized = previous.clone();
for (n, c) in normalized.components.iter_mut().zip(&self.components) {
n.metadata = c.metadata.clone();
n.properties = c.properties.clone();
let mut out = self.clone();
out.metadata.clone_from(&other.metadata);
// Paired by position: if the components themselves differ, so do the designs.
for (c, o) in out.components.iter_mut().zip(&other.components) {
c.metadata.clone_from(&o.metadata);
c.properties.clone_from(&o.properties);
}
*self == normalized
Some(out)
}
/// The colour a body is shown and exported in (sRGB as 0–1 fractions): its appearance,
@@ -3105,6 +3241,9 @@ impl Document {
if self.components.len() >= 10_000 {
return Err(DocError::Invalid("too many components".into()));
}
self.normalise_component_color_indices()?;
let color_index = self.next_component_color_index;
let next_color_index = color_index.checked_add(1).ok_or_else(|| DocError::Invalid("component colour sequence exhausted".into()))?;
let id = self.next_id.max(1);
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
let name = match name.map(str::trim) {
@@ -3117,7 +3256,8 @@ impl Document {
format!("Component{k}")
}
};
self.components.push(Component { id, name, parent, ..Default::default() });
self.components.push(Component { id, name, parent, identification_color_index: Some(color_index), ..Default::default() });
self.next_component_color_index = next_color_index;
let occurrence = self.add_occurrence(id, parent, crate::IDENTITY)?;
// The design's first part is grounded, so joints and drags have something fixed to work
// from (#2804); later parts and copies start free. Only creation decides this: opening a
@@ -3469,6 +3609,12 @@ impl Document {
/// Add or change a parameter. Fails if the result would not evaluate. A new parameter
/// without a unit takes the unit of its value (mm, deg or none).
pub fn set_param(&mut self, name: &str, expr_s: &str, unit: Option<&str>, comment: Option<&str>) -> Result<()> {
// A text parameter: one with the text unit, or an existing one when no unit is given.
match (unit.map(crate::text_params::is_text_unit), self.text_param(name).is_some()) {
(Some(true), _) | (None, true) => return self.set_text_param(name, expr_s, comment),
(Some(false), true) => return Err(DocError::Invalid(format!("`{name}` is a text parameter"))),
_ => {}
}
if let Some(p) = Self::param_name_problem(name) {
return Err(DocError::Invalid(p));
}
@@ -3583,12 +3729,19 @@ impl Document {
let b = self.resolve_plane_in(vals, base, depth + 1, st)?;
Ok(b.offset(Self::eval_in(vals, distance, Kind::Length)?))
}
PlaneRef::AtAngle { base, axis_origin, axis_dir, angle } => {
PlaneRef::AtAngle { base, axis_origin, axis_dir, angle, line } => {
let b = self.resolve_plane_in(vals, base, depth + 1, st)?;
let a = Self::eval_in(vals, angle, Kind::Angle)?;
let (axis_origin, axis_dir) = match line {
Some(g) => {
let st = st.ok_or_else(|| DocError::Invalid("this plane is built from the model: evaluate first".into()))?;
crate::construct::model_axis(st, g)?
}
None => (*axis_origin, *axis_dir),
};
let d = axis_dir.normalized().ok_or_else(|| DocError::Invalid("rotation axis".into()))?;
let rot = |v: Vec3| v * a.cos() + d.cross(v) * a.sin() + d * (d.dot(v) * (1.0 - a.cos()));
let o = *axis_origin + rot(b.origin - *axis_origin);
let o = axis_origin + rot(b.origin - axis_origin);
Plane::new(o, rot(b.x), rot(b.y)).ok_or_else(|| DocError::Invalid("degenerate plane".into()))
}
PlaneRef::Flipped { base, about_y } => Ok(self.resolve_plane_in(vals, base, depth + 1, st)?.flipped(*about_y)),
@@ -3637,6 +3790,7 @@ impl Document {
/// parameters (in place).
pub fn apply_dimension_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
self.apply_point_values(vals, sk)?;
self.apply_pattern_values(vals, sk)?;
for c in &mut sk.constraints {
if let Some(p) = &c.param {
let kind = if c.kind.is_angle() { Kind::Angle } else { Kind::Length };
@@ -3668,6 +3822,43 @@ impl Document {
Ok(())
}
/// Evaluate the retained pattern inputs before solving its associative relations.
pub fn apply_pattern_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
use solidcraft_sketch::SketchPatternKind;
self.apply_pattern_quantities(vals, sk)?;
for index in 0..sk.patterns.len() {
let Some(pattern) = sk.patterns.get(index) else { continue };
let mut kind = pattern.kind.clone();
for (key, expression) in &pattern.expressions {
let unit = match key.as_str() {
"count" | "count2" => Kind::Unitless,
"spacing" | "spacing2" => Kind::Length,
"angle" => Kind::Angle,
_ => return Err(DocError::Invalid(format!("unknown sketch pattern input `{key}`"))),
};
let value = crate::eval::val(vals, expression, unit).map_err(|e| DocError::Expr(format!("pattern {} `{key}`: {e}", pattern.id)))?;
let count = || -> Result<usize> {
if !(1.0..=500.0).contains(&value) || value.fract() != 0.0 {
return Err(DocError::Invalid(format!("pattern `{key}` must be a positive integer from 1 to 500")));
}
Ok(value as usize)
};
match (&mut kind, key.as_str()) {
(SketchPatternKind::Circular { count: n, .. } | SketchPatternKind::Rectangular { count: n, .. }, "count") => *n = count()?,
(SketchPatternKind::Rectangular { count2, .. }, "count2") => *count2 = count()?,
(SketchPatternKind::Rectangular { spacing, .. }, "spacing") => *spacing = value,
(SketchPatternKind::Rectangular { spacing2, .. }, "spacing2") => *spacing2 = value,
(SketchPatternKind::Circular { angle, .. }, "angle") => *angle = value.to_degrees(),
_ => return Err(DocError::Invalid(format!("invalid sketch pattern input `{key}`"))),
}
}
if kind != pattern.kind {
sk.update_pattern(index, kind)?;
}
}
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<()> {
@@ -3689,14 +3880,14 @@ impl Document {
Ok(())
}
/// Give the texts of a sketch the numbers their expressions come to (in place): a text's
/// height, place and angle that were typed as expressions. A text's outlines are rebuilt from
/// its numbers when the sketch's links are refreshed, so this comes before that. A number
/// typed as a number is left as it is.
/// Give the texts of a sketch the numbers and words their expressions come to (in place): a
/// text's height, place and angle that were typed as expressions, and its words built from
/// text parameters. A text's outlines are rebuilt from them when the sketch's links are
/// refreshed, so this comes before that. What was typed as such is left as it is.
pub fn apply_text_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
use crate::eval::val;
for link in &mut sk.links {
let solidcraft_sketch::LinkSource::Text { at, height, angle, exprs, .. } = &mut link.source else { continue };
let solidcraft_sketch::LinkSource::Text { text, at, height, angle, exprs, .. } = &mut link.source else { continue };
let number = |e: &Option<String>, kind: Kind, what: &str| -> Result<Option<f64>> {
match e {
None => Ok(None),
@@ -3718,6 +3909,13 @@ impl Document {
if exprs.height.is_some() && !(*height > 1e-6 && *height < 1e5) {
return Err(DocError::Invalid("a text's height must be positive".into()));
}
if let Some(e) = &exprs.text {
let words = self.eval_words(e).map_err(|error| DocError::Expr(format!("text: {error}")))?;
if words.trim().is_empty() || words.chars().count() > solidcraft_sketch::MAX_TEXT_CHARS {
return Err(DocError::Invalid(format!("text: `{e}` gives no words, or too many")));
}
*text = words;
}
}
Ok(())
}
@@ -3756,7 +3954,6 @@ 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();
+504 -126
View File
File diff suppressed because it is too large Load Diff
+76 -19
View File
@@ -67,11 +67,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if st.bodies.is_empty() {
return Err(DocError::Invalid("there is no body to emboss".into()));
}
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
apply_op(doc, st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
}
FeatureKind::Rib { .. } => {
let tools = rib_tools(vals, st, f, None)?;
apply_op(st, f, tools, Operation::Join, &[])
apply_op(doc, st, f, tools, Operation::Join, &[])
}
FeatureKind::ReplaceFace { faces, target, body } => {
let tp = doc.resolve_plane_in(vals, target, 0, Some(st))?;
@@ -561,7 +561,7 @@ pub(super) fn sheet_eval(
st.sheets.push(sh);
Ok(())
}
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief, height_from } => {
let i = sheet_for(st, body, edges.first().map(|e| e.point.0))?;
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
// Each edge's own height and angle, else the feature's.
@@ -598,6 +598,11 @@ pub(super) fn sheet_eval(
None => None,
},
};
let from = match height_from {
Some(name) => sheet::HeightFrom::parse(name)
.ok_or_else(|| DocError::Invalid(format!("unknown flange height datum `{name}` (outer, inner or tangent)")))?,
None => sheet::HeightFrom::Outer,
};
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
if sh.flat {
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
@@ -606,7 +611,7 @@ pub(super) fn sheet_eval(
let each: Vec<sheet::EdgeFlange> = sheet_picks(sh, f, &points, warning)
.into_iter()
.zip(own)
.map(|(pick, (height, angle))| sheet::EdgeFlange { pick, height, angle })
.map(|(pick, (height, angle))| sheet::EdgeFlange { pick, height, angle, from })
.collect();
let n0 = sh.flanges.len();
let (_, notes) = sh.add_flanges_each(&each, rr, position, *flip, relief)?;
@@ -928,13 +933,61 @@ pub(super) fn sheet_cut(
constructor: Option<&ExtrudeConstructor>,
warning: &mut Option<String>,
) -> Result<Vec<String>> {
let FeatureKind::Extrude { sketch, profiles, operation, extent, .. } = &f.kind else { return Ok(Vec::new()) };
if extent.taper.is_some() || !any_eligible_sheet(f, st, targets) {
if !any_eligible_sheet(f, st, targets) {
return Ok(Vec::new());
}
let Some(ss) = st.sketch(sketch.0).cloned() else { return Ok(Vec::new()) };
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
// The outlines of the sketch's text: the sheet remembers where its letters went.
let Some(cut) = extrude_cut(st, f)? else { return Ok(Vec::new()) };
sheet_prism_cut(f, st, &cut.plane, &cut.regions, cut.operation, tools, targets, constructor, cut.bound, &cut.text, warning)
}
/// A copy of the extrude cut `src` that the pattern `f` makes, moved by `m`: machined into the
/// sheets as the cut itself is, in the moved sketch plane (#6841). `tools` are the copy's. Only
/// for a cut moved without turning over: a mirrored copy, and any other feature, comes back
/// unhandled (no sheets) for the caller to apply as before.
pub(super) fn sheet_cut_copy(
f: &Feature,
src: &Feature,
m: &Mat,
st: &mut ModelState,
tools: &[Body],
targets: &[String],
warning: &mut Option<String>,
) -> Result<Vec<String>> {
if !any_eligible_sheet(f, st, targets) {
return Ok(Vec::new());
}
let Some(cut) = extrude_cut(st, src)? else { return Ok(Vec::new()) };
if cut.operation != Operation::Cut {
return Ok(Vec::new());
}
let (x, y) = (crate::apply_vector(m, cut.plane.x), crate::apply_vector(m, cut.plane.y));
if x.cross(y).dot(crate::apply_vector(m, cut.plane.normal())) <= 0.0 {
return Ok(Vec::new());
}
let Some(plane) = Plane::new(apply_point(m, cut.plane.origin), x, y) else { return Ok(Vec::new()) };
sheet_prism_cut(f, st, &plane, &cut.regions, cut.operation, tools, targets, None, cut.bound, &cut.text, warning)
}
/// What an untapered extrude cut (or intersect) machines into a sheet.
struct ExtrudeCut {
plane: Plane,
regions: Vec<Region2>,
/// The outlines of the sketch's text: the sheet remembers where its letters went.
text: Vec<Seg2>,
operation: Operation,
/// Up to a face: the tools' own footprints bound it.
bound: bool,
}
/// The sketch plane, regions and text of `f`'s extrude; None for any other feature, a tapered
/// extrude or a missing sketch.
fn extrude_cut(st: &ModelState, f: &Feature) -> Result<Option<ExtrudeCut>> {
let FeatureKind::Extrude { sketch, profiles, operation, extent, .. } = &f.kind else { return Ok(None) };
if extent.taper.is_some() {
return Ok(None);
}
let Some(ss) = st.sketch(sketch.0) else { return Ok(None) };
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
let text: Vec<Seg2> = ss
.sketch
.links
@@ -943,7 +996,7 @@ pub(super) fn sheet_cut(
.flat_map(|l| ss.sketch.link_curves(&l.id))
.flat_map(|ci| ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci)))
.collect();
sheet_prism_cut(f, st, &ss.plane, &regions, *operation, tools, targets, constructor, extent.to.is_some(), &text, warning)
Ok(Some(ExtrudeCut { plane: ss.plane, regions, text, operation: *operation, bound: extent.to.is_some() }))
}
/// Whether a cut of `f` with these targets (else its participants, else its component's
@@ -1884,7 +1937,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?.into_iter().map(|b| crate::lineage::part_tool(b, &format!("rib{k}"))));
}
}
apply_traced(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
apply_traced(doc, st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
// The root fillet: rounds the circle where the post meets the face.
if rf > 1e-9 {
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
@@ -1908,7 +1961,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let hp = Plane::new(position.0, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
apply_traced(doc, st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
}
Ok(())
}
@@ -1994,25 +2047,28 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let delta = (h * 0.05).clamp(1e-3, 0.5);
if *groove {
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?.into_iter().map(|b| crate::lineage::part_tool(b, "groove")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Cut, std::slice::from_ref(&target))
} else {
let tool = kernel::extrude(&plane, &[band], -delta, h)?.into_iter().map(|b| crate::lineage::part_tool(b, "lip")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, base_height, new_body, body } => {
let target = body_under(st, body, position.0)?;
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)?);
let bh = base_height.as_deref().map(|e| size("base_height", e)).transpose()?.unwrap_or(0.0);
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);
// Side profile (x = toward the catch, y = up the arm), extruded across the width.
let pts =
[Vec2::new(-t, -delta), Vec2::new(0.0, -delta), Vec2::new(0.0, l - cl), Vec2::new(cd, l - cl), Vec2::new(0.0, l), Vec2::new(-t, l)];
// The base reaches `base_height` below the face; a joined arm bites `delta` into
// the wall, a separate body sits flush on it.
let y0 = if *new_body { -bh } else { -delta - bh };
let pts = [Vec2::new(-t, y0), Vec2::new(0.0, y0), Vec2::new(0.0, l - cl), Vec2::new(cd, l - cl), Vec2::new(0.0, l), Vec2::new(-t, l)];
let o = position.0 - y * (w / 2.0);
let pl = Plane::new(o, x, z).ok_or_else(|| DocError::Invalid("snap fit frame".into()))?;
let mut lp = Loop2::polygon(&pts);
@@ -2023,7 +2079,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let (lo, hi) = if sgn > 0.0 { (0.0, w) } else { (-w, 0.0) };
let tool = kernel::extrude(&pl, &[Region2 { outer: lp, holes: Vec::new() }], lo, hi)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "arm")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
let op = if *new_body { Operation::NewBody } else { Operation::Join };
apply_traced(doc, st, f, tool, op, std::slice::from_ref(&target))
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, position.0)?;
@@ -2074,7 +2131,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
kernel::extrude(&base, &[region], 0.0, h + delta)?
};
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "rest")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
apply_traced(doc, st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
_ => Err(DocError::Invalid(format!("{} is not a plastic feature", f.name))),
}
+4 -2
View File
@@ -65,7 +65,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if tools.is_empty() {
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
}
apply_op(st, f, tools, *operation, &crate::plain(targets))
apply_op(doc, st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::SurfaceTrim { body, plane, tool, keep, keep_more, curves, with, keep_with, stitch, extend } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
@@ -304,7 +304,9 @@ 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, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
let (o, d) = revolve_axis(&ss, axis, &super::axis_state(doc, st, f.component, 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), _) => {

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