Compare commits

...

271 Commits

Author SHA1 Message Date
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
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
bherbruck 72ad6383ef Release 0.22.1 2026-10-25 10:38:39 -04:00
bherbruck 2cf05388f1 Release 0.22.1: version bump, design format 36 (#6628)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:38:36 -04: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
bherbruck 093571cfe4 Connect AI apps at start-up, with a preference to turn it off (#6388)
At launch the app adds the solidcraft entry to each installed AI client off the window thread,
names newly connected clients once, and refreshes a stale path. Settings > Assistant has
"Make SolidCraft available to AI apps on this computer" (on by default); off removes the entries it
added. Entries the user changed are left alone; automation runs never touch the files.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:51:55 +00:00
sascha2101 a1b9ca13e3 Edit Feature: a primitive in a moved component keeps its placement (#2926) (#5989)
* claim: #2926

* Edit Feature: a primitive in a moved component keeps its placement (#2926)

Edit Feature passes a primitive's placement (corner, base, axis, center)
through as saved, in its component's frame, as timeline.inspect reports it.
timeline.redefine read it as a world point and mapped it through the
component's placement, so in a turned component the edit moved the
primitive: in the Hinge Assembly, Leaf B's rod flipped and the revolute
joint put the leaf on the other side.

A placement equal to the saved one now stays as saved; a new one is still
mapped from the world.

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

* Move the #2926 tests up in frames_tests.rs

They sat at the end of the file, where main has since added the #6000
test; moving them 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-25 13:00:40 +00:00
Colin Faulkingham 699f526429 Show imported mesh edges in shaded and wireframe views (#5662) (#5740)
* claim: #5662

* Show imported mesh outlines in viewport (#5662)

* test: update mesh regression origin visibility setup
2026-10-25 12:54:31 +00:00
Colin Faulkingham 16a5254e97 Fix macOS fullscreen application-bar interaction (#3643) (#3690)
* claim: #3643

* fix: keep macOS fullscreen application bar clickable (#3643)

* test: load bundled fonts for fullscreen regression
2026-10-25 12:49:02 +00:00
semmelrinde e5613ebfdf Placing and meshing: no threads started for bodies that are done already (#6406)
Every command in a design with placed components places their bodies
again on up to 16 threads, though nearly all of them are taken as they
were placed before (the same body by the same matrix). Those are now
taken on the calling thread and only the rest are placed on threads.
Warming the meshes (and display plans) of a state likewise starts
threads only for the bodies that have none yet.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:48:00 +00:00
bherbruck dfde1e032d mcp setup: add SolidCraft to the AI clients on this computer; plain mcp finds a late app (#6379)
solidcraft-cli mcp setup (alias install) and Help > Connect AI assistants edit each installed
client's own configuration in place, with this program's absolute path: OpenCode, Claude Desktop,
Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed and VS Code. Only the solidcraft
entries change; a stale path is refreshed and an entry the user changed is left alone.

Plain mcp keeps looking for the app while its headless session has no work, and says on every
tool result that it is headless when the app was not found.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:57:51 +00:00
github-actions[bot] 10da6b9efc Regenerate generated files and fold pending files [skip ci] 2026-10-25 12:27:09 +00:00
Ahmad Khan 5a5a98ee80 Sheet metal: Path Flange, a chain of bends on an existing sheet edge (#4094) (#4096)
* claim: #4094

* Sheet metal: Path Flange, a chain of bends on an existing sheet edge (#4094)

Path Flange (`sheet.path_flange`): sketch an open chain of lines across a sheet edge, pick the
edge and one line; each line becomes a flange as long as the line and each corner a bend, one
feature and one body. The edge flange takes further bends (`then`), each on the far edge of the
one before, and the Flange dialog shows them as rows with a bin each.
2026-10-25 11:33:42 +00:00
Mikalai Kukhta 4e2270517a Release: sign and notarize the macOS app (#5749) (#6016)
* Release: sign and notarize the macOS app with a Developer ID (#5749)

package.sh --sign signs the app, the CLI and the DMG with the hardened runtime and a secure
timestamp, notarizes the DMG and the CLI zip, staples the DMG and checks it with stapler and
spctl. The app carries entitlements.plist: com.apple.security.cs.disable-library-validation
lets the hardened runtime load 3Dconnexion's client framework (crates/threedx-client).

signing.sh --setup imports the certificate into a temporary keychain and writes the API key;
--cleanup removes both. The release workflow builds both architectures first (macos-build, no
secrets), then signs and packages in the join job. Without the APPLE_* secrets (forks, dry runs)
the app keeps its ad-hoc signature and the job summary says so.

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

* macOS release: staple the app before the DMG, retry stapler, document renewal (#5749)

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:15:35 +00:00
Colin Faulkingham 0312808b92 DXF: reject non-BMP text instead of replacing it (#5056) (#5709)
* claim: #5056

* fix: reject unsupported Unicode in DXF text (#5056)

* fix: adapt new DXF callers to fallible writer (#5056)
2026-10-25 11:01:32 +00:00
fkirill 9d45fa6b34 Respect design units across length fields (#3271) (#3315)
* claim: #3271

* fix(ui): use document units across length fields (#3271)

* Format

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

* Adapt drag-extrude and joint tests to document-unit length helpers

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

* fix(ui): preserve blank parameter drafts and cover bounded lengths

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:24:30 +00:00
Frank e4f11c8df6 Preview angled construction planes (#5697)
* Preview planes at an angle

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Plane at Angle: test the preview when an existing plane is edited

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

* ui: include ghost setting in plane preview test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: close angle plane regression test block

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:04:57 +00:00
Jesper We bbc3aa4301 Offset Plane: through a point (#3765)
* claim: Offset Plane through a point

* Offset Plane: through a point instead of at a distance
2026-10-25 06:02:14 +00:00
Jesper We 611d4eb6b8 Crash reports: a repeated panic that ends the app is counted, not reported as an unclean exit (#6051) (#6052)
* claim: #6051

* Crash reports: a repeated panic that ends the app is counted, not reported as an unclean exit (#6051)
2026-10-25 06:00:57 +00:00
Kai Evans c50f5364c6 Selection fields: only the active one looks active (#5256) (#5259)
* Selection fields: only the active one looks active

* Selection fields: a filled field keeps its tinted border where a theme fixes the control border's colour
2026-10-25 05:47:23 +00:00
Kai Evans beb38b44aa Select Other: Shift and Ctrl add the choice to the selection, as a click does (#5258) (#5261)
* Select Other: Shift and Ctrl add the choice to the selection, as a click does

* Select Other: follow origin visibility rename in test

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

* Select Other test: click away from the lines' midpoints, which are picks of their own now

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:47:20 +00:00
Ahmad Khan a49698ee3d Value fields step with the arrow keys and the mouse wheel (#2320) (#2608)
* claim: #2320

* Value fields step with the arrow keys and the mouse wheel (#2320)

Up/Down in a focused value field step a plain number by 1 in the unit it is written in
("20 mm" -> "21 mm", "2,5 mm" -> "3,5 mm"), 10 with Shift and 0.1 with Alt; the wheel over
the focused field does the same, a step a notch, instead of scrolling the panel under it.
Expressions such as `width / 2` are left as typed, and an open completion list keeps the
arrows. Every expression field gets this through `params_dialog::complete`: the value box
beside the geometry in every dialog, the dialog rows, sketch dimensions, the dimension
editor, Drive Joint and the Parameters window.

* The value box beside the geometry stays under the pointer

The box rides on the arrow's tip, so each wheel notch that stepped its value moved it, and
after a notch or two the box slid out from under the pointer and the wheel zoomed the view
instead. While the pointer is over it the box now keeps its place, and it goes back to the tip
when the pointer leaves.

* Value stepping is a preference, off by default

Preferences › General › "Step values with the arrow keys and the wheel" (`step_values`,
also through `ui.prefs`) turns on Up/Down and wheel stepping in value fields, and the value
box staying under the pointer while the wheel steps it. Off, as by default, value fields
behave as before: Up/Down move the text cursor, the wheel scrolls, and the box beside the
geometry follows its arrow.

* Value stepping test: Bend Radius is under Advanced now

* Value stepping test: the preference is on the Design page

* Value stepping test: draw the field straight into the test's frame (#2320)

The shared outer-host rule now covers tests too; the field needs no panel around it.
2026-10-25 05:34:03 +00:00
Ahmad Khan 919f54a23e Sheet metal: bend angles as the included (press-brake) angle, set per rule (#4968) (#6040)
* claim: #4968

* Sheet metal rules: bend angles as the included angle (#4968)

A sheet metal rule's angle_display (bend, the default, or included) sets
how the DXF bend notes read each bend: a 58° bend is B1 UP 122° R1 under
an included rule. The bend table always gives included_angle_deg beside
angle_deg (CSV and JSON). Set in sheet.manage_rules and the Sheet Metal
Rules dialog, listed by sheet.rules; the flange angle stays the bend angle.

* Bend table: the included angle as the CSV's last column, so the columns before it keep their places (#4968)

A test also covers a body whose base flange names its rule, which keeps that rule's notes whichever rule is active, and a bend down.

* Sheet Metal Rules: Angle Display reads Bend or Included, so the choice fits its box (#4968)

The rule editor's row contract lists the new row and the key the dialog sends.

---------

Co-authored-by: begsaquib <saquibbeg4444@gmail.com>
2026-10-25 05:11:13 +00:00
Colin Faulkingham 1d0e371e64 Preserve lid edge references after parameter changes (#5580) (#5658)
* claim: #5580

* fix: follow named lid edges after parameter changes (#5580)

* fix: initialize viewport test frame tints

* Drop duplicate tint fields in gpu view tests (main already has them)

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 05:04:24 +00:00
Matt Van Horn 43565cd33b Snap drawing to where two sketch curves cross (#4967) (#5342)
While drawing, the pointer snaps to the crossing of two lines, circles or
arcs and keeps the new point on both curves. Shift still prefers a midpoint.
2026-10-25 04:50:20 +00:00
raykholo ab9d9ac50f Manage > Units opens the unit picker (#5907) (#5908)
* claim: #5907

* Fix Manage Units to open the shared unit menu (#5907)

Open the browser's existing unit choices for interactive command starts
instead of executing document.units without its required parameter. Anchor
the menu below toolbar buttons and menu rows. Choices still run the normal
command, while programmatic calls require an explicit unit.

Cover error-free opening, unit choices, button anchoring and dismissal,
the current-unit marker, one-step undo/redo, and programmatic calls.

Fixes #5907
2026-10-25 04:35:33 +00:00
rbousigues88 157c0d68d0 Sketch Dimension: point/circle to circle or arc edge (#4064) (#4065)
* Sketch Dimension: point or circle to the edge of a circle or arc (#4064)

Adds PointCircleDistance and CircleCircleDistance (edge-based, like #1863):
solver, constructive seed, drawing, command wiring, tests, UI scenarios and docs.
Saved designs gain two constraint kinds, in one pending format file.

Fixes #4064.

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

* Sketch Dimension scenario: pick the circle at its top edge (#4064)

The bottom edge sits under the Dimension tool's status hint in the scenario window, so the second click never reached the tool.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 04:29:28 +00:00
raykholo 38d7ea4198 Agent chat: each design tab keeps its own conversation (#5439) (#5612)
* claim: #5439

* Agent chat: each design tab keeps its own conversation (#5439)

* Adapt chat-per-tab changes to the new Pending context and cursor tests

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 04:20:54 +00:00
dudedagger42 7c2b2f1559 Revolve takes a planar body face as its profile (#2747) (#5170)
solid.revolve already revolves a planar body face (`face`, `face_body`), but
the Revolve window's Profile input took sketch profiles only, so a face
couldn't be picked. The input now takes planar faces too, and a picked face
is sent as `face`; sketch profiles win when both are picked.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 04:06:08 +00:00
raykholo cc1e654690 Warn when a design is open elsewhere or changed on disk (#5467) (#5468)
* claim: #5467

* Warn when a design is open elsewhere or changed on disk (#5467)

The desktop app keeps a lock file next to each open design
(.~<file>.lock: computer, user, version, opened and refreshed times),
refreshes it every minute and removes it on close; a lock not
refreshed for three minutes is stale and ignored. Opening a design
that another SolidCraft has open asks: Open Read-Only, Open Anyway or
Cancel. A read-only design saves only to another file.

A design remembers its file's size and modification time when it is
opened or saved. Save over a file another program or computer changed
since asks: Save a Copy (the default), Overwrite or Cancel.

Programmatic calls never ask: doc.open reports open_elsewhere and takes
read_only, and file.save over a changed file returns an error until it
is given overwrite: true. ui.documents answers both questions.
2026-10-25 03:34:20 +00:00
raykholo 7af97e3729 Editing an Extrude or Revolve shows its sketch (#5336) (#5443)
* claim: #5336

* Editing an Extrude or Revolve shows its sketch (#5336)

A finished sketch is hidden once a feature uses it, and it stayed hidden
while that feature's dialog was open for editing, so no other profiles
could be picked. The session now holds the sketch of the feature being
edited; the edit dialog sets it while it is open and clears it however
it closes, and that sketch is drawn without changing stored visibility.
2026-10-25 03:26:36 +00:00
raykholo 138bb0261c Windows OpenGL: use the system title bar so the picture lines up with clicks (#2063) (#4403)
* claim: #2063

* Windows OpenGL: use the system title bar so the picture lines up with clicks (#2063)

On Windows with only OpenGL to start, the window opens with the system
title bar instead of using the application bar as its title bar. The
window without OS decorations keeps its caption style while having no
frame, and some OpenGL drivers (older Intel HD/UHD) show the picture a
caption's height lower than the window, so clicks land above what they
point at. Direct3D 12 and Vulkan keep the custom title bar.

The choice is made before the window opens, from the backends wgpu
starts, so the window never changes its frame while OpenGL draws into it.
2026-10-25 03:19:34 +00:00
raykholo dbe7dfa4bc Report a bug: title from the workspace and the reporter's words (#5377) (#5445)
* claim: #5377

* Report a bug: title from the workspace and the reporter's words (#5377)

With no dialog open the report was always labelled "Modeling" and its
summary pre-filled as "unexpected result", so every such report arrived
titled "Modeling: unexpected result" and the fallback to the first line
of "What happened?" never applied.

Label the report from the active workspace (Part, Sketch, Surface, Sheet
Metal, Plastic, Mesh, or Render), leave the summary empty so the title
becomes "<workspace>: <first line of the description>", and keep
"operation failed" when an error was recorded. The summary field now
hints that a short title is optional.

The triage script that asks for an empty in-app report to be pasted
recognises the new titles.
2026-10-25 03:03:08 +00:00
bherbruck 1ceab2ed4c One file per change for new UI strings and attribution rows (#6448)
New UI strings go in assets/locales/pending/<slug>.json and new ATTRIBUTION.md rows in
attribution/pending/<slug>.md. The app build, xtask lists/i18n/assets read them as part of the
catalogs, and the Generated files workflow folds and deletes them on main with
cargo xtask fold-pending.

Refs #6439

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:01:41 +00:00
wckdboy 743accca7a Triangulation: split a crossing boundary segment ourselves, never in spade's assert (#4031) (#5409)
* claim: #4031

* Triangulation: split a crossing boundary segment ourselves, never in spade's assert (#4031)

A trimmed face's boundary segment that crosses a constraint already in the
triangulation went to spade's add_constraint_and_split, which asserts
(cdt.rs:978, `!previous_region.is_empty() || from == final_vertex`) when a
split lands so near another vertex or constraint that the piece up to it is
blocked. The kernel guard caught it, but the whole operation failed and a
crash report was written; a STEP body imported empty or not at all (#4598,
also #4293, #4478, #5068, #5229).

PolyBoundary::add_constraints now adds every segment that can be added, then
splits each blocked one itself: a vertex at each crossing, inserted from the
crossed edge's own vertex, and the pieces between added where they can be. A
segment with crowded or very many crossings, or a face with more than four
blocked segments (a rim that runs back and forth across itself), leaves them
out, as truck did before #1653, so the face meshes slightly off instead of
not at all. Pinched faces (#1648) still split where they cross.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:55:24 +00:00
bherbruck 128692da0c Agent chat: no per-message limit by default (#6349)
* Agent chat: no per-message limit by default

The limit is optional now: an Unlimited checkbox on the AI page (checked by default), a
number field when unchecked. A saved 24, the old default, reads as unlimited. The API-key agent's
24-request and the gate's 400-request caps are gone; Stop still cancels.

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

* Agent chat: add the new messages to the complete Japanese and Chinese catalogs

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:54:13 +00:00
rbousigues88 13b4f6e98c Viewport: a selected edge's highlight no longer shows as a wedge on a grazing face (#3544)
* Viewport: lift selected and hovered edge highlights toward the eye

A selected edge's highlight (3 px core, 6 px halo) is a screen-space band that
was depth-tested at the edge's own depth. A face next to the edge seen at a
grazing angle is pushed back by only about a pixel of its slope, so it buried
part of the band, unevenly along the edge: the highlight showed as a thin
wedge instead of a line.

The band is now drawn like a thin cylinder around the edge: its depth is
pulled toward the eye by half its width plus a pixel, measured on screen so
it holds at every zoom. Plain model edges keep their depth (#1434 still
holds); the part of a highlighted edge the model really hides stays dimmed
through hidden_line.

Not yet compiled or tested.

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

* Viewport: lift an edge highlight by how steeply its faces are seen

The first lift (half the band's width plus a pixel, the same on every edge)
was not tied to the faces beside the edge. Each highlight segment now carries
the normals of the two faces it borders (octahedral, packed with its width in
the line's width slot, read as u32 by vs_line): the band is pulled toward the
eye by its half width times how steeply those faces are seen, so a grazing
face no longer buries it, and an edge seen square to its faces is not lifted
at all.

Test: a_selected_edge_on_a_grazing_face_draws_whole (tilts 60 to 88 degrees,
small and large designs, orthographic and perspective; the band 5 mm behind
a face stays hidden). Capturer::rgba_highlighted draws a highlight scene as
the viewport does, for that test.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:48:34 +00:00
raykholo ac53fc1e6f Viewport: bodies stay visible after switching to perspective and back (#5422) (#5614)
* claim: #5422

* Viewport: bodies stay visible after switching to perspective and back (#5422)
2026-10-25 02:46:59 +00:00
Homero Thompson 8d63a9f4cb Theme: the viewport keeps the previous colours until the camera moves (#5802) (#5835)
* claim: #5802

* fix: repaint on a theme change and key the frame cache on the theme tints (#5802)

theme::apply asks for a repaint, so the new visuals are painted on the next pass instead of waiting for another event to schedule one. The gpu frame cache key now includes the theme's hover and selection tints, so a picture cached under the previous theme's colours is not reused.

* test(ui): settle egui's startup repaints before asserting the theme's repaint (#5802)

* test(ui): name the call site that asks for the theme's repaint (#5802)

* test(ui): apply the theme outside a pass so its repaint request stands alone (#5802)
2026-10-25 02:46:41 +00:00
bherbruck a150822d9b Put solidcraft-cli on PATH from the AppImage and the macOS app (#6347)
The AppImage writes a ~/.local/bin/solidcraft-cli launcher from its desktop-integration step
(same opt-outs, never over a file it did not write). The macOS app links the CLI into
/opt/homebrew/bin or /usr/local/bin at launch when one is writable, and Help > Install
solidcraft-cli command does it with an administrator prompt otherwise. Snap and Flatpak are documented.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:45:45 +00:00
raykholo a9fd4b1b05 A dragged command window keeps its place (#5357) (#5444)
* claim: #5357

* A dragged command window keeps its place (#5357)

Command dialogs share one dragged offset per dock side for the session
instead of one per title, and closing a dialog no longer forgets it, so
the next command opens where the last one was left. The offset is still
kept inside the viewport, including after the window shrinks.
2026-10-25 02:38:48 +00:00
amolinari-autotex face5e2d31 Hole: independent tapping depth and recessed drill points (#4486)
* feat(hole): checkpoint independent tapped thread depth

* test(hole): cover independent tapped thread depth boundaries

* fix(doc): bind tapped depth to the useful cylindrical wall

* feat(hole): dimension independent thread depth in the section editor

* fix(hole): preserve custom tapping controls after main integration

* feat: add recessed-hole drill points and order depth dimensions

* fix(hole): match the current preferences and UI kit

* fix(hole): verify reference placement with independent tapping depth
2026-10-25 02:31:24 +00:00
Benny1496 efd0e70f7d fix(oracle): use curvature-weighted centroid moments for case 33 (#5442)
* fix(oracle): use curvature-weighted centroid moments for case 33

* style(oracle): preserve reference JSON numeric formatting

* docs(oracle): remove transient case 33 validation note
2026-10-25 02:30:39 +00:00
SiroArt 3f4c6e4571 Display refinement waits until the camera stands still (#3595) (#5446)
Choosing and swapping in a refinement rebuilds and uploads the whole model
scene. During an orbit of a large assembly that happened about ten times a
second, tens of milliseconds each. It now waits until the camera has stood
still for 200 ms.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 02:30:36 +00:00
rvmzym5xv8-star abc39685b1 Parameters: name the limit an out-of-range number or over-long expression broke (#5906)
Part of #4712.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:30:04 +00:00
Colin Faulkingham f93913b3d3 Sketch: accept curve snaps for Midpoint Line (#5981) (#6041)
* claim: #5981

* fix(sketch): resolve midpoint line snaps (#5981)
2026-10-25 02:29:47 +00:00
ivisaf 765c5fd143 Pictures drawn off the window thread finish before the process exits (#6062) (#6063)
A Home picture still being drawn by the detached saved-thumbnail thread
could be on the GPU as the process exited, and the process died with
SIGSEGV inside the graphics driver. capture::close stops new pictures
and waits for those in flight (the app calls it in on_exit), and
dropping SolveApp waits for the Home picture threads it started.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:40 +00:00
Colin Faulkingham 6494f8fe18 AutoConstrain: dimension construction geometry (#6173) (#6185)
* claim: #6173

* fix(sketch): dimension construction curves in AutoConstrain
2026-10-25 02:29:37 +00:00
selune 1623034973 Render: gate bundled studio-data tests in isolated engine builds (#5493) (#6202)
* claim: post-merge Render review corrections (#5493)

* fix(render): gate bundled studio tests in isolated engine builds (#5493)

Addresses jyviko review comments on #5494: compile the two positive payload-dependent regressions only with bundled-render-library. Keep all assertions and the explicit feature-absent refusals. Reproduced 5 pass / 2 fail before the fix; isolated default, CPU without pack, and CPU with pack tests plus strict scoped Clippy and repository checks pass. No production or CI configuration changes.
2026-10-25 02:29:34 +00:00
NOWN-DEV 74004841b4 Sketch text: a text on a path follows the path in the same evaluation (#6215)
* Test: a text on a path follows the path when a parameter moves it

A construction line placed by a dimension that reads a parameter, and a text laid along it: after
the parameter changes the line has moved and the text has not. It follows one evaluation late.

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

* Sketch text: a text on a path follows the path in the same evaluation

Problem: texts are laid out with the sketch's links, before the sketch is solved. A solve that
moves the curve a text runs along (a path placed by a parameter) left the text where the path had
been before the change, so it followed one evaluation late.

Solution: after a solve that keeps the sketch's orientation, every text that runs along a curve
of its sketch is laid out again along that curve as the solve left it; a text that no longer lays
out adds its warning.

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-25 02:29:31 +00:00
NOWN-DEV be08517de2 Construction: a plane on a sheet's flat face follows the face (#6217)
* Test: an offset plane on a sheet flange's face follows the flange

An offset plane on the inner face of an edge flange, picked by body and index as model.faces
lists it. When the base gets wider the flange moves along x and the plane stays where the face
was picked; a sketch on the same face follows it.

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

* Construction: a plane on a sheet's flat face follows the face

Problem: a construction plane on a flat face of a sheet body resolved the stored plane, which is
only where the face was when it was picked. When the sheet changed the plane stayed behind, while
a sketch on the same face follows it by its sheet face name.

Solution: a construction plane on a sheet's flat face resolves that face's plane as it is now, as a
sketch on the face does.

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-25 02:29:28 +00:00
NOWN-DEV 24ed21998b Configurations: a design keeps up to 2048 rules (#6221)
* Test: a design keeps several hundred configuration rules

An assembly that switches a few hundred parts on and off, by two rules each, needs hundreds of
rules; the saved-rule budget refused more than 128.

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

* Configurations: a design keeps up to 2048 rules

Problem: a configuration table kept at most 128 rules. A rule drives one feature's suppression,
so an assembly that switches a few hundred parts on and off (by two rules each) could not keep
its rules. Copying rules for an independent component copy looked up each new id by scanning all
rules, which a larger table makes slow.

Solution: MAX_RULES is 2048; a copy looks its new ids up in a set of the ids taken. The docs say
the new limit.

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-25 02:29:25 +00:00
NOWN-DEV 3592031d5e fix: joints on an imported part's hole or face stay on that part (#6368)
The parts of one STEP assembly import share their face names, and each
part's body stays where the file has it, placed in its component by an
offset. A joint origin on a named face (a hole's centre, a face's
centre, an x reference by face, the end a round origin takes) looked the
name up on every body of the design and compared the pick, in the
joint's component coordinates, with each body as it lies. Another part's
face of that name could lie nearer, and the joint resolved on that
part's face: a pin jointed into one plate's hole landed where the other
plate's hole is relative to its plate.

named_face now looks only at the bodies of the joint's component, each
in its own coordinates, a face the name finds exactly before a piece of
it or a face it was merged into, then the nearest. The design-wide
lookup stays for a face whose body has gone to another component.
named_round, x_axis, pin_end and end_faces take the component, as
face_name_at and round_end_at already do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:16 +00:00
NOWN-DEV b8d9db7440 fix: a face-centre joint origin stays put when a hole in its face moves or goes (#6370)
A joint origin at a face's centre (`joint.origin {face}`, `Snap::FaceCenter`) was the face's
area centroid, holes included. A hole cut into that face, moved or suppressed therefore moved
every part held at the face's centre, though the face's outline stayed where it was: on a
200 x 100 plate with a 20 mm hole, 0.31 mm for each 20 mm the hole moves and 0.62 mm when the
hole, 40 mm off the middle, is suppressed.

On a planar face with holes the origin is now the middle of the area the face's outer boundary
encloses (`outline_centre`: the face's triangle sides that no other triangle of the face runs
back along, corners matched by place, chained into loops in the face's plane; the centroid of
the loop enclosing the most area). A named face (`NamedFace::plane`) and the planar face
nearest a point (`nearest_plane`) both use it. A face without holes, or one whose mesh gives no
clean outline, keeps its centroid as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:14 +00:00
NOWN-DEV 2b620338df perf: solve the joints once per parameter change (#6372)
A parameter change evaluates the model and solves the joints inside the
command (so that it can refuse a value that breaks a sketch), and
`Session::execute` refreshes again after the command and solves every joint a
second time, although nothing has moved since. On 40 small parts jointed
rigidly to a plate's holes (half of the joints lifted by an expression),
changing the lifts' parameter took 0.85 s on main and takes 0.45 s now;
changing one part's thickness, 1.96 s and 1.52 s (CLI, medians of 5 runs).

The session now keeps the design the joints were last solved for, as the
solve left it, and the model that solve read. A refresh that finds the same
model, and a design that differs from the solved one at most in its features,
leaves the joints as they are: the joints read features only through the
model, and a value edit stores the new solutions of the sketches it drove
after its solve, so its design is a new one by the time of the second
refresh. Placements and bounding boxes come out identical.

Test: `a_parameter_change_solves_the_joints_once` counts one solve per change
(two before), checks the jointed part still follows the hole, and that a
captured move of a jointed part is still solved back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:11 +00:00
NOWN-DEV a5b6408587 fix: an edge joint origin on a body placed by an offset lands on that edge (#6374)
An edge joint origin looked its edge and face up in the body's own coordinates with the pick in the component's,
and returned the frame in body coordinates. On a body placed by an offset (an imported part's bodies are) a pick on
the edge where it is shown found no edge ("joint origin: no edge there"), or the origin sat away from its edge by
the offset. Edge origins now take the picks into the body's coordinates through the inverse of its offset and place
the frame back by it, as named faces and round origins already do. A body without an offset is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:29:08 +00:00
bherbruck 78de687902 CI: duplicate-PR check and a 'Changes a default' line (Refs #6439) (#6442)
* CI: duplicate-PR check and a required 'Changes a default' line (Refs #6439)

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

* PR checks: enforce the defaults line only on PRs opened from 2026-10-25 (Refs #6439)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:28:57 +00:00
bherbruck 2741cd9a8b Section Analysis: edges behind the cut no longer show through the cap (#6328) (#6363)
Since #1768 the section's stencil marking draws every face with FACE_BIAS.
Where a back face seen through the cut meets a front face along an edge (a
pocket's wall and its side wall or floor, a fillet and the face it runs
into), the slope push decided which was nearer beside that edge; a front
face winning left those samples unmarked, the cap skipped them, and the
edge, drawn at its own depth, showed through the hatch. In perspective, and
while orbiting in orthographic too, edges popped in and out from frame to
frame.

A second marking pass draws the back faces only (front faces culled, depth
test <=, no depth write), pulled toward the eye as far as FACE_BIAS pushes
faces back, before the cap. The depth buffer keeps FACE_BIAS, so edges on
visible faces still draw whole (#1434).

Tests in crates/gpu/tests/depth.rs: a pocket 2 mm behind the cut, and a
filleted hub with a bore and a cross hole seen from twelve directions, both
depths, orthographic and perspective. On main the pocket shows 45 px of
edges through the cap in perspective and the hub 25 px over the views; with
this change 0 and at most 2.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 02:03:31 +00:00
raykholo 86f5500914 Sketch: arcs and circles honour construction when they are created (#5352) (#5440)
* claim: #5352

* Sketch: circles and arcs honour construction (#5352)

sketch.arc.center_point, sketch.arc.three_point, sketch.circle.two_point,
and sketch.circle.three_point accepted construction: true but drew normal
geometry. They now mark the new curve as construction, like lines and
rectangles, and their parameter docs list construction?.
2026-10-25 01:56:44 +00:00
DuBearte 7804f9938a Intersection curves: a point that won't project no longer panics (#5562) (#5812)
* claim: #5562

* Intersection curves: a point that won't project no longer panics (#5562)

A quarter disc at a box's corner revolved 360 deg about the box's edge and
cut (a hemisphere whose pole sits on that edge) panicked in truck at
intersection_curve.rs:133: dividing one of the cut's intersection edges
met a point that projects onto neither surface, and the hinted
projection's fallback called subs, which unwraps that same failed
projection. The guard turned it into a failed cut and a crash report.

Now the leader's point stands in, the division stops there, and the
failure is recorded (truck_geometry take_projection_failure). truck-shapeops'
face division checks it after dividing an edge and gives up, so the
Boolean attempt fails as quickly as the panic made it and the kernel goes
on to its other tries: carrying the approximate points on instead made one
kernel test (equal bores crossing) run for minutes. On this design the cut
still fails with an ordinary error, leaving the box whole; nothing panics.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:23 +00:00
DuBearte ab03aa5bc5 Sketch: explicit planes take a frame (world or component) and the reply says which (#6000) (#6015)
* claim: #6000

* Sketch: explicit planes take a frame, world or component (#6000)

sketch.create and sketch.redefine take frame: "world" (the default,
as before) or "component" for an explicit {origin, x_dir, y_dir}
plane. The reply says which frame the plane was read in, with a note
when a world plane goes into a moved or turned component. frame on a
named plane or a face is refused: those are where they are.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:10 +00:00
semmelrinde 2e3f27f06e Extrude: the automatic operation is decided where the extrude starts (#467) (#6104)
With Start set to From Object (or a start offset), the automatic
operation was still decided from the profile: the engine probed for
material just past the profile plane, and the dialog kept the operation
it had latched before the start changed. A profile on a plate's top,
extruded 14 mm down from the plate's bottom face, came up as Cut instead
of Join. The engine now probes from the extrude's start (and, to an
object, reaches from there), and the dialog decides the operation afresh
when the start changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:04 +00:00
DuBearte bda32419bd Windows: a stuck icacls no longer freezes SolidCraft when it writes the control token (#6145) (#6182)
* claim: #6145

* Windows: a stuck icacls no longer freezes SolidCraft when it writes the control token (#6145)

Writing the control token runs icacls to drop the file's inherited
permissions, and waited for it without a limit. On some computers the
new icacls process stays suspended, so connecting an agent froze the app
for good. It now waits at most 5 s; an icacls still running then is ended
and the token written anyway, into the user's own local app data folder.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:56:01 +00:00
NOWN-DEV c242c484a8 Construction: the root's constructions are found once the design has a component (#6219)
* Test: a plane through a construction axis follows it in a design with a component

Two offset planes, the axis where they meet and a plane through that axis. Once the design has a
component, a parameter change fails the last plane with "unknown construction `Ax`".

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

* Construction: the root's constructions are found once the design has a component

Problem: once a design has a component, the root's construction features are built in the model
placed in the world. That state carried each component's bodies, sketches and threads, but not
its construction planes, axes and points, so a construction built on another one failed on every
evaluation with "unknown construction".

Solution: the world state carries each component's constructions where the component is placed
(moved by its occurrence's placement, as the bodies are).

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-25 01:55:56 +00:00
NOWN-DEV 5d37cb01d5 Configurations: config.rule takes several rules at once and checks the rows once (#6223)
* Test: configuration rules given together are checked with all of them in place

Suppressing a box and the cut on it: the box's rule alone fails the row check (the cut has
nothing left to cut), so the two rules can't be added one at a time. Given together they pass,
in one undo step; a bad rule among them adds none.

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

* Configurations: config.rule takes several rules at once and checks the rows once

Problem: config.rule adds one rule and checks every saved row with it. Rules that only hold
together (suppressing a body and the cut on it) could not be added at all: the first one alone
fails the check. A design with many rules also rebuilt every row once per rule.

Solution: config.rule takes `rules: [...]`: each rule is added or replaced as with `rule`, then
the rows are checked once with all of them in place, in one undo step; a rule that fails
validation adds none. The docs describe it.

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-25 01:55:54 +00:00
NOWN-DEV 7313fd8684 Joints: an origin takes a face name as long as the face's piece name (#6225)
* Test: joint origins take a long piece name

A face split by a cut whose edge runs in many segments is named after every face around its
piece, several hundred characters. joint.origin refused such a name, as model.faces gives it,
over 512 characters, both for a face centre and as a round end.

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

* Joints: an origin takes a face name as long as the face's piece name

Problem: joint.origin and joint.create took a face name (a face centre, a round end, an x
direction) only up to 512 characters. A face that a cut split is named after the faces around its
piece (`<face>#[<face>,…]`), which reaches several hundred characters when the cut's edge runs in
many segments (a slot with a zigzag edge, a cut of text), so a name model.faces gives was refused.

Solution: the face names a joint origin takes are capped at 16384 characters (MAX_FACE_NAME),
still a bound on hostile input.

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-25 01:55:52 +00:00
NOWN-DEV 6f340ade69 Sheet metal: a refolded sheet's faces keep the names of the faces they were (#6268)
* Sheet metal: test that an unfolded sheet's flat face is named after its base at every size

An unfold keeps the base in place and lays the flanges out beside it; the flat
face spanning them and the bend strips is the base's face. It was named after
the unfold whenever few of its sample points fell on the base (the bend strips
are many small triangles, the base two big ones), and that changed with the
sizes: a size change renamed every top and bottom face the refold made of it,
and joints and sketches on them lost their faces (40 of 85 joint ends of a
real part after one change). The test unfolds and refolds a base with three
flanges at two flange heights and wants the flat face and every refolded
panel face named after the base.

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

* Sheet metal: the panel an unfold or refold keeps in place names the face it is part of

A face a sheet feature made is named by where its sample points lie on the
input's faces: in place, else taken back through the folds. Only nine points
per face are sampled, triangle centroids picked by index. An unfolded sheet's
flat face spans the base, the flanges laid out beside it and the bend strips
between them; its bend strips are many small triangles, so most samples fell
on them. Those lie on no panel, and one of them made the fold fallback give
up for the whole face. The face was named after the unfold, and the refold
passed that name to every top and bottom face; which way it went changed with
the part's sizes.

For a sheet feature that moved panels, a face the plain vote leaves unnamed
is now looked at closer (up to 256 points): if a part of it lies in place on
the body's own face before (the panel the feature kept), that face names it.
Else the fold fallback votes over the points that can be taken back, when at
least half can, instead of none when one cannot. Other features, and faces
the plain vote names, are named as before.

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-25 01:55:49 +00:00
NOWN-DEV e260437846 Names: a pattern copy's piece names match as the original's do (#6270)
* Test: a pattern copy's piece names match as the original's do

A copy's faces carry the instance after the name, after any piece qualifier
(`top#[left,slot:a]@F9.1`). A stored piece name of a copy whose bounding faces have changed since
finds no face of the copy at all (None at the first assertion), where the original's identical
name matches by its stem. On a real part: the joint ends on a patterned copy fall back to their
picks after a size change, the original's resolve by name.

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

* Names: a pattern copy's piece names match as the original's do

Problem: a pattern's copy names its faces after the original's with the instance appended
after any piece qualifier (`top#[left,slot:a]@F9.1`). The piece parser expects a qualified name
to end with its qualifier, so a copy's name was its own stem: when the faces around a piece
changed, a stored name of a copy matched no face of the copy at all, where the original's
identical name still matched by its stem. Joint ends on a patterned part fell back to their picks
after a size change and flipped to the other end of their holes.

Solution: `history_names::matches` takes the pattern instances off the end of both names first
and compares the rest as before, within the same copy only (equal instances; an original's name
never matches a copy's face, nor one copy's another's).

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-25 01:55:47 +00:00
NOWN-DEV f54ad0f12f Configurations: a parameter edit runs the suppression rules whose condition reads what changed (#6303)
* Test: a switch parameter alone switches the features its rules suppress

Configuration rules that switch features' suppression from a parameter (drive_suppression when
`on` = 0) ran only on config.activate: a plain parameters.change of the switch left the features
as they were, so a script had to activate a row for every change. Red until the fix.

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

* Configurations: a parameter edit runs the suppression rules whose condition reads what changed

Problem: configuration rules ran only when a row was activated or a configuration edited. A
design that switches features on and off by a parameter (a rule that suppresses a feature when
the parameter is 0) did not follow a parameters.change of that parameter.

Solution: parameters.change (one parameter or several) runs, before the rebuild, the enabled
rules that switch a feature's suppression and whose condition reads a parameter whose value
changed (or a feature switched earlier in the same pass), in their order as activation applies
them, and lists them as `switched`. Driven parameters and restrictions still wait for a
configuration edit or activation; a rule whose condition reads nothing that changed leaves a
hand-set suppression alone; a rule that cannot be evaluated is skipped and never blocks the edit.
One undo step with the edit. docs/configurations.md says so.

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-25 01:55:44 +00:00
semmelrinde 43f21e8fec Sketch drag: a live frame stops once its numerical solves have done their share (#6330)
A live drag frame shares one work budget between its held solves, but
only their cluster decomposition was charged. The numerical solve (LM)
ran outside it, so in a larger sketch where the geometry can't follow
the cursor, one frame ran 64 failing held solves of about 150 ms each:
10 s per frame (68 points, 50 curves, 58 constraints, found by
randomised sketch editing). The frame budget now also counts the LM
work of its held solves, without stopping a solve: once a frame's share
is spent, the drag takes no more steps in that frame, keeps what it
reached and asks for the next frame (retry), as the viewport already
handles. The same drag now takes about 0.3 s per frame. Solves outside
live frames are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:42 +00:00
semmelrinde 3e1f1a89f8 Assemblies: owners of bodies looked up once per placement pass, body frames only when picked (#6356)
Placing a design's bodies asked each body's owning component once per
placement, and each answer searched the features for the one that made
the body: placements x bodies x features on every command, twice (the
world state and the frames that map a command's picks into components).
Document::model_body_components answers for all bodies at once, and
world_state_reusing and body_frames_with_source use it. The engine's
frames now work out the per-body table only when a command picks a body
or names one; most commands pick none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:39 +00:00
semmelrinde 82253c6f35 Expressions: remember what an expression names and a constant parameter's value (#6360)
Every evaluation fingerprints every feature by the parameters its
expressions name, and works out every parameter, and most of them (named
feature inputs) are literal values such as "10 mm". In a design built
command by command both were lexed and parsed again on every command.
references() now keeps its answers per thread (bounded), eval_params
keeps its last few results for identical definitions (compared in full),
and a parameter that names no other is evaluated once per expression,
unit and design length unit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:37 +00:00
NOWN-DEV f4d937d8a2 parts: a standard part's sketches are fully defined (#6376)
A standard part (a nut, washer, bolt, screw, pin or bearing) drew its sketches without
constraints, so each was left with free degrees: 3 for a hex, 8 to 12 for a revolved outline.
A check of a design that holds parts listed every one of them as not fully defined. The part
builder now fixes each sketch's points and curves before finishing it: the outline is the
part's size, and a resize builds it again the same way.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:34 +00:00
semmelrinde 75fb613e25 Names: a new feature's and input's number found without searching the design per number (#6377)
A new feature is named after the first free number of its stem
(Cylinder1, Cylinder2 …), and every number tried searched all features'
names: the thousandth cylinder of a design tried a thousand names against
a thousand features. A new input's dN name likewise wrote out every d1,
d2 … it tried. Both now gather the numbers taken after their stem once
(params::numbers_after, which counts only names written as the number
is) and take the first number not among them. The names are the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:31 +00:00
semmelrinde ce614044a7 Evaluation: a feature's fingerprint text is written only when the feature changed (#6397)
Every evaluation fingerprints every feature by its JSON, so a design
built command by command wrote all of its features out as JSON on every
command. The text is now kept (per thread, by feature id) with the
feature it was written from, and written again only when the feature
differs; the text hashed is the same, so the fingerprints are too.
Imports, whose text depends on their STEP digest, are written each time
as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:29 +00:00
semmelrinde 91549d29fb Inspect: bodies measured on all cores, and once while they stay the same (#6412)
document.inspect with measure (what inspect_design and the
solidcraft://design resource ask for) measured every body one after
the other, and again on every call: on a 1,280-body design each call
took 25 to 60 s. It now measures them on all cores, as
inspect.measure does, and each body keeps its measurement in a cell
shared by its clones (with the body it was measured on, as the outline
box is kept), so a design inspected again measures only the bodies that
changed. Placements taken as before keep their placed body's cell too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:23 +00:00
NOWN-DEV b6a5a9166a perf: a pass over the joints evaluates the design's parameters once (#6418)
Every joint whose offset, angle or values are expressions is worked out again
on each solve, twice per solve (re-expressing the joints, then the check that
leaves out one whose expression no longer works out), and each expression that
names a parameter evaluated every parameter of the design anew
(`Document::eval`). The cost grows with the joints with expressions times the
design's parameters: on 40 parts jointed to a plate, each joint lifted by an
expression and each part carrying 13 parameters (522 in all), a joint solve
took 0.55 s on main and takes 0.41 s now, a change of the lifts' parameter
1.06 s and 0.81 s (CLI, medians of 5 runs).

`joints::Params` keeps the design's parameter values once the first
expression names one, and `joints::expressed` takes it, so one pass evaluates
the parameters once. Values and error messages are those of `Document::eval`.

Test: `kept_parameters_evaluate_as_the_design_does` compares values and
errors with `Document::eval`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:55:20 +00:00
NOWN-DEV 0063954286 Sketch: a sketch whose links moved far is walked there and keeps the stored sides and turns (#6420)
* Test: a label on a path follows its source far

A rectangle of construction lines placed on three points projected from another sketch, with a
centred text on its far side: moved 270 away by the parameters that place the source, its solve
from the stored sketch failed ("the sketch constraints conflict").

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

* Sketch: a sketch whose links moved far is walked there and keeps the stored sides and turns

Problem: a change upstream moves what a sketch projects (or the points its parameters place)
while its own values stay. The solve from the stored state then either failed ("the sketch
constraints conflict") or landed on the mirror image of a side or turn and reported it solved,
because a solve after a link move checks no orientation when no value changed: a line tangent to
a moving circle hopped a diameter to the circle's other side. And a sketch moved so was stored
only when its own values changed, so every later evaluation solved it from its first stored state
again.

Solution:
- flips_from_stored: the orientations of the stored sketch (unsigned offsets, distance
  triangles, and a line's side of a circle it is tangent to) that a solved sketch turned over,
  each read where the stored sketch has its points.
- solve_after_link_move: the new values solved where the links were stored, then the moved
  points walked to where they are now (predictor-corrector: each step starts from the last two
  solutions carried along their secant; steps halve on a failure or a flip and grow while they
  solve).
- Document::solve_sketch: when a linked or fixed point moved and the jump fails or flips, the
  walk; a flip the walk cannot avoid either is reported as one.
- Model::edited_sketches: a sketch whose linked points a value edit moved is stored as solved, so
  the next evaluation starts where this one ended.

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-25 01:55:17 +00:00
Colin Faulkingham 6f6b64d484 Export Current View as DXF downloads in the browser (#3431) (#3601)
* claim: #3431

* fix(engine): use browser clock for DXF view export (#3431)
2026-10-25 01:49:58 +00:00
leValdemar19 52e496d017 Sketch Symmetry: centre a line between two fixed lines (#1796) (#3895)
* claim: #1796

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

* Sketch Symmetry: centre a line between a face's edges (#1796)

Symmetry of two edges of a face (its projected outline, whose sides run
in opposite directions) about a construction line between them was
refused with "neither entity can move". The pairing of the line ends by
mirror image landed on main in #3029; this keeps the tool side and the
tests for #1796:

- The Symmetry tool and a three-line preselection take the only
  construction line or centerline as the symmetry line, whatever the
  pick order (picking it first used to mirror it out past the other
  line). Two points and a line take the line. Otherwise the last pick,
  as before.
- Tests: fixed lines drawn the opposite way, pairs by mirror image (a V,
  end to end along the axis), every pick order, a three-line
  preselection, and a scenario clicking a face's edges.

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 01:49:54 +00:00
CamWight d557c09c3b Sketch text: measure a line with the rule that draws it (#4813) (#4815)
* claim: #4813

* Sketch text: measure a line by the rule that draws it (#4813)

`TextMetrics`, made once from a font and a style, parses the face once and gives the scale for a capital height, the
distance between lines, a line's width and the characters the font has no glyph for (in order, each time, including
those its character map sends to `.notdef`). `text_geometry_styled` lays its lines out through it: one rule for
measured and drawn text, no kerning, a missing character still drawn as `?`. Measuring builds no outlines, so a text
too large to draw (`MAX_LINK_ENTITIES`, `MAX_TEXT_CHARS`) can still be measured. Every outline stays bit for bit the
same: a test pins five styled texts, captured before the change. The size and spacing checks are shared, and still
run before the font is looked for; a text whose place or angle is not finite now says so instead of naming its
height.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:49:22 +00:00
raykholo 7046f7f0e7 Windows resize smoke test: wait for a settled window before hit testing (#5523) (#5524)
* claim: #5523

* Windows resize smoke test: wait for a settled window before hit testing

Startup placement can move the window between CheckBorder reading its rect
and the hit tests, so correct answers for the new position failed against
stale coordinates. Wait for two reads 200 ms apart to agree before the first
pass, compare the rect after each pass, and retry the pass (up to three tries)
when it moved. A pass with a stable rect fails exactly as before.

Fixes #5523
2026-10-25 01:49:14 +00:00
Homero Thompson 16286249ed Extrude: a picked region expands across the reference geometry that cuts the drawn profile (#5302) (#5530)
* claim: #5302

* Extrude: a picked profile takes the whole drawn profile (#5302)

A sketch on a solid's side face holds the face's outline as projected
reference geometry, and that outline cuts the drawn profile into pieces
of the sketch's graph. A pick by index or by point resolved against those
pieces, so extruding a rectangle that straddles the face's edge took
only the piece under the cursor.

The pick is now expanded to the drawn profile it belongs to, so it takes
every region of it. The drawn profiles are the disjoint faces of the
drawn graph, so an explicit pick of a drawn hole still fills only that
hole (#5176).

* Extrude: a pick expands across the reference geometry that cuts the drawn profile (#5302)

The expansion crossed every region of the drawn profile, so a projected curve that bounds a region inside the profile (the rim of a counterbore) was crossed too and the cut took more material than intended; now the expansion stops at that boundary. Fixes the regression in cmd::features::prism_local_tests::a_ring_cut_round_a_counterbored_hole_in_a_chamfered_disc, and keeps the behaviour of #5302 and of the #5176 invariant.
2026-10-25 01:48:50 +00:00
rlartot 19e2800472 Sketch on the sides of a thin extrude (#2722) (#5781)
* claim: #2722

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

* Sketch on the sides of a thin extrude (#2722)

A thin wall's sides lie off the sketch curves (half the thickness either
side, say), so no curve was under them: its side faces had no role, no
exact history name, and Create Sketch refused them as "can't keep an
attachment". The roles of a thin wall's sides are now
`wall:<curve>:left|right` (the side of the curve it runs beside, in the
direction the curve is drawn), and its open ends `wall:<curve>:start|end`.
They follow the thickness and side settings and survive a save.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:48:35 +00:00
DuBearte a5f7f5a921 Batch: consecutive parameter edits can rebuild the design once (#5822) (#5839)
* claim: #5822

* Batch: consecutive parameter edits can rebuild the design once (#5822)

An agent's batch of parameter edits, one parameter per step, evaluated the
design once per edit: an edit early in the timeline rebuilds nearly all of
it, and 21 such edits ran for minutes.

engine.batch and the MCP batch tool take coalesce_parameters (default
false). With it, each run of consecutive plain parameter edits
(parameters.change with only a name and an expression, or with changes) is
set as one parameters.change {changes}: one rebuild for the run. Any other
step ends a run, so later steps see the design up to date. Every step
still gets its own result (coalesced: the run's length); the run is one
undo step. A run whose combined edit is refused (a new parameter, which
changes can't make; a bad expression) runs step by step, so a refused edit
is still named by its own step. The app's off-thread evaluation computes
the run the same way. The grouping is solidcraft_engine::batch_params,
shared by both runners.

21 parameter edits on a plate with 21 extruded bosses (in-process MCP,
ci profile): 40.1 s step by step, 3.5 s coalesced, the same volume.

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

* Batch coalescing: say a run skips the states between its edits; test it against step by step (#5822)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:31 +00:00
Frank a402c5e502 Snap points: the centre, middle and ends an edge or face offers, in the engine (#5881) (#5882)
* claim: #5881

* Snap points: the centre, middle and ends an edge or face offers (#5881)

Phase 1 of #5881. `solidcraft_engine::snap::snap_points` gives the points
a picked item offers as picks, from Measure's exact shapes: a round edge
its centre (an arc also its ends and exact middle, joined across pieces
the kernel split it into), a straight or other edge its ends and middle,
a flat face the centre of its area, a cylinder or cone the centres of its
ends, a sphere or torus its centre, a vertex itself. Nothing uses them yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:29 +00:00
JIJINBEI cdde7d62b2 Persistent names read by one parser; four misreadings fixed (#5814) (#5966)
* doc: persistent names read as their structure; joint.inspect splits an edge with a blend face first

`persistent_name::{FaceName, EdgeName}` parse a face or edge name once (blends, inner walls,
copies, pieces, numbered pieces, `~k`) and write it back as the same text, bounded against
hostile input. A suffix after a blend's edge naming a later feature belongs to the blend face.

joint.inspect split an edge at its first `|`, so an edge whose first face is a blend
(`F4:blend:F3:+x|F3:+z#[…]|F5:-y`) came back as a cut-off blend name and a string that is no
face. It now uses `EdgeName::parse`.

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

* doc: persistent names as typed values: features, roles, suffixes, a lexer and errors that say where

`FaceName` is a base (made by a feature in a role, a blend and the edge it rounds, an inner
wall, a body's face by index, other text) and its suffixes (copy, piece, numbered piece).
Roles are typed where the vocabulary is known (top, side:<curve>, +x, face:<k>, …) and kept as
written otherwise. The text form is lexed into tokens and read by the grammar in the module's
doc; a name that doesn't read says where and what was expected.

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

* names: read what made a face, its kind and its feature ids through persistent_name

The places that took a name apart by hand to find the feature that made a face, whether it is
a blend, a copy, a numbered piece or a wall, or to remap feature ids on copy, now ask the typed
name (`made_by`, `split_feature`, `is_blend`, `is_copy`, `is_numbered_piece`, `is_wall`,
`features`, `map`). A name that doesn't read falls back to the old text check.

- An offset of a face that only names a blend inside it (`F2:top#[F5:blend:…]`) is no longer
  taken for a fillet's.
- Copying a component remaps feature ids inside a piece's `#[…]` too.
- A shell's inner wall split by a later cut keeps the qualifier on the wall.
- joint.inspect finds the sketch curve of a copied side (`side:l1@F7.2`).

Finding a face by name (strip_piece, parent_piece) is unchanged: it reads a trailing
qualifier as the whole name's and moves once the text form says where it belongs.

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

* engine: regression tests for press-pull on a wall that only names a fillet and joint.inspect on a copied side

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

* names: an edge holds one face or more; made_by reads only the leading token

An open boundary's edge has one face and an edge several faces meet at has more, so an edge name
is a list of faces (a blend's own edge still takes two at most, as before). `made_by` reads just
the leading `F<n>:`, which every name a feature made starts with, so asking it of every face
each frame stays cheap. Names are read up to 1 MiB: a face engraved with text has piece names
past 4 KiB.

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

* persistent_name: split into lexer, parser, write, text and tests

No code changed; callers' paths stay the same (`pub use text::*`).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:26 +00:00
DuBearte 3f6b9fe89f Timeline: reorder anyway (allow_errors) and report the features the move breaks (#5962) (#5988)
* claim: #5962

* Timeline: reorder anyway (allow_errors) and report the features the move breaks (#5962)

timeline.reorder and timeline.reorder_many refuse a move after which a
feature that worked would fail. Sometimes breaking a later feature is the
point: an agent restructuring a design wants the move made, then the list
of what broke so it can repair those features next.

Both take allow_errors (default false: refused as before). With it the
move is made and the reply lists broken: [{feature, error}] (empty when
nothing broke); one undo puts the order back. timeline.reorder_many also
takes active, as timeline.reorder does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:23 +00:00
Nathalie Schönwetter b4acd83c78 A count from a length says how to get a count (#5901) (#6011)
A pattern count such as (L-2*Post)/210 is a length when the 210 is unit-less, and was refused with only "expected a unit-less number, got a length". The message now says to divide by a length, e.g. floor(L / (210 mm)).

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:48:21 +00:00
semmelrinde f7233c4418 Render: a preview setup with exposure renders and names what it leaves out (#5089) (#6073)
render.settings.set stores exposure_ev and the environment controls, but
render.preflight and render.start refused any setup with them because the
CPU image executor doesn't apply them, so such a setup could never render.
The CPU executor now renders it and the replies list the left-out
controls in ignored_controls.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:48:20 +00:00
Colin Faulkingham 5573ae06fc Prevent body renames from reusing removed body names (#6042) (#6048)
* claim: #6042

* fix: reject body names retained by Remove (#6042)
2026-10-25 01:48:15 +00:00
raykholo 89a4a96630 Ribbon: an active contextual tab looks selected (#4230) (#4242)
* claim: #4230

* Ribbon: an active contextual tab looks selected (#4230)

Contextual tabs (Sketch while editing, Sheet Metal for a sheet body) chose
their solid accent fill before checking whether they were active, so
selected and unselected looked the same.

The selected contextual tab keeps the solid accent pill. An unselected one
now looks like any unselected tab with its accent as an outline, so it still
marks the context without outshining the tab that is selected. Ordinary tabs
are unchanged.
2026-10-25 01:26:47 +00:00
raykholo 34d1ac0376 A straight 3D sketch line can be picked as an axis (#4363) (#4381)
* claim: #4363

* A straight 3D sketch line can be picked as an axis (#4363)

3D sketch wires were always offered as curved, and axis inputs resolved
only 2D sketch lines, so a straight 3D line couldn't be an axis (Plane at
Angle and others). A single-segment wire is now offered as straight and
resolves to its endpoints in model space; splines are still refused.
2026-10-25 01:26:44 +00:00
Thodoris Tsagkogeorgas 0e9e175e79 Two-sided sketch offset shows both distances in the sketch (#4385) (#4386)
A two-sided offset drew only the Distance dimension. Distance 2 now gets
its own value box and arrow, the other way from Distance.
2026-10-25 01:26:41 +00:00
rlartot f11ad0ce28 Ribbon tooltips: written help for six more tools (#2758) (#4592)
Combine, Loft, Mirror, Circular pattern, Rectangular pattern and Sweep get the
same two-sentence help as the first six (en, fr, ja, zh). Text only.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:26:33 +00:00
neosandeep24 9ad96fb81b Home: keep a removed recent design removed (#4660) (#4718)
Removing a design from the Start page's recent list only changed memory, and on the web sync_recents() rebuilt the list from stored designs on each launch, adding it back. Track removed designs in a persisted 'forgotten' set that sync_recents honours; opening or saving a design, or deleting/renaming its file, clears it.
2026-10-25 01:26:31 +00:00
Herperlerpicus b671265faa Viewport: show the running tool's icon beside the pointer (#4869) (#4870)
* Viewport: show the running tool's icon beside the pointer

While an interactive tool or a command's dialog runs, its toolbar icon
rides above and right of the pointer in the viewport, so the next click's
command is visible without looking up at the ribbon. It sits clear of the
system arrow and of the sketch tools' coordinate readout below the
pointer, moves left at the right edge and below the readout at the top.

It shows only where a click reaches the model: not over panels, dialogs,
menus or the navigation bar, and not while the camera moves. The icon is
the command's own (the one the ribbon and the dialog header show), so
there is no new artwork. On by default; "Show tool at cursor" in View
settings (`ui.set {"toolCursor": false}`) turns it off.

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

* Viewport: the tool at the cursor is off by default

As asked in review: the badge starts off, and "Show tool at cursor" in
View settings (or `ui.set {"toolCursor": true}`) turns it on. The test
checks the default and turns it on through the View settings checkbox.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:26:27 +00:00
Reefwalker 832ff941d1 Edit Text: explain failed bold previews immediately (#5004) (#5155)
* claim: #5004

* fix: explain failed sketch text previews immediately (#5004)
2026-10-25 01:26:23 +00:00
raykholo 9869997733 Sketch Create menu: line, rectangle, and other families show their icons (#5244) (#5279)
* claim: #5244

* Sketch Create menu: family rows show their icons

The Line, Rectangle, Circle, Arc, Slot, Polygon, and Spline entries in a
sketch's Create drop-down were drawn as plain text buttons. Draw each as
the shared menu row with its first variant's icon and a submenu arrow,
keeping the same popup handling for its variants.
2026-10-25 01:26:20 +00:00
raykholo 745a24bbc8 Sketch: undo while drawing a spline removes the last point, not the sketch (#5304) (#5441)
* claim: #5304

* Sketch: undo and Backspace remove the last pending spline point (#5304)

While a spline tool has points placed, Cmd/Ctrl+Z and Backspace take back
the last point instead of undoing sketch history or deleting the selection.
Undo reaches sketch history again once no spline points remain.
2026-10-25 01:26:15 +00:00
raykholo 26340882c0 Mirror keeps the preselected feature when the object type changes (#5288) (#5453)
* claim: #5288

* Mirror keeps the preselected feature when the object type changes (#5288)

The pre-selection moves into the objects input when a pattern window
opens, and switching Object Type cleared that input, so switching to
Bodies and back lost the selected feature. The window now keeps the
objects of the type not shown and restores them when it is chosen
again. A plain timeline click on a feature already picked keeps it;
Ctrl+click takes it out.
2026-10-25 01:26:13 +00:00
raykholo 8e2b8f2ace Floating history: the last done feature is no longer drawn as selected (#5415) (#5455)
* claim: #5415

* Floating history: the last done feature is no longer drawn as selected (#5415)

The floating history strip passed the last feature before the marker to
HistoryChip as selected, so it looked selected with nothing selected.
Pass only the real selection, and mark the last done feature with a small
tab-accent dot under its chip instead.
2026-10-25 01:26:10 +00:00
ISHII YUKI 34e75979b2 Agent chat: hand WSL paths to agents started through wsl.exe (#5546) (#5549)
* claim: #5546

* Agent chat: hand WSL paths to agents started through wsl.exe (#5546)

An ACP agent started through wsl.exe was handed the session's working
folder and the solidcraft-app bridge's command as Windows paths, which it
can't use: the Claude adapter refuses the session, and the bridge never
starts. When the agent's program is wsl or wsl.exe, convert both to their
WSL form (C:\Users\... -> /mnt/c/Users/...) before session/new, in the
chat and in the Test button. The bridge still runs on Windows and reaches
the app at 127.0.0.1.
2026-10-25 01:26:05 +00:00
N3MI-DG d5eca6cc78 3D mouse: let the overall speed slider reach the 5x the engine allows (#5704)
The Preferences > Navigation overall speed slider stopped at 3.00x, but
`space_mouse::frame` clamps the applied speed to 0.1..=5.0, so the top of
the range the engine honours could not be reached from the UI. Raise the
slider's ceiling to 5.0x to match that clamp; the step, format and lower
bound are unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-25 01:25:58 +00:00
rlartot 77c77829e3 Preferences: Z up orientation figure is drawn right-handed (#5680) (#5741)
* claim: #5680

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

* Preferences: draw the Z up orientation figure right-handed (#5680)

Z up showed Y pointing toward the viewer on the left, a mirrored
(left-handed) figure. Y now points away on the right, like the axis
gizmo. The axis tips move into orientation_axes and a test checks that
X to Y and Y to Z turn counter-clockwise in both figures.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:55 +00:00
Colin Faulkingham f01ef3c6aa Keep the Parameters window height stable when moved (#5078) (#5752)
* claim: #5078

* Keep the Parameters window height stable when moved (#5078)
2026-10-25 01:25:53 +00:00
rlartot f5a6c8fa51 Link the version number to its release notes (#5488) (#5765)
* claim: #5488

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

* Link the version number in the title bar to its release notes (#5488)

The version at the right of the title bar opens the release page of this
build (v<version>, which exists for stable, candidate and nightly
releases) in the browser; a development build opens the release history.
Its click area stops at the report button, which the long version
strings of candidates and nightlies overlap.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:51 +00:00
rlartot 865575e410 Hem: the handle runs along the hem to its tip (#5828) (#5844)
* claim: #5828

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

* Hem: the handle runs along the hem to its tip (#5828)

The hem's on-canvas handle started on the picked edge and pointed square
to the sheet (#4794 only flipped it), so it neither sat on the hem nor
moved the way the Length grows. Length is measured from the outer face
of the fold to the tip, so the handle now starts on the centre line of
the hem layer at that face and points along the hem toward the tip.

The test builds the hem for both Flip states and checks the handle's
direction, its start on the layer's centre line, and that start plus
Length lands on the built tip. It replaces the arrow-side test of #4836,
whose rule (the arrow points to the side the hem builds on) no longer
applies.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 01:25:49 +00:00
HiroKenEz 992c93fe5b Find in Timeline scrolls both timeline layouts to the found feature (#5850) (#5863)
* claim: #5850

* Find in Timeline scrolls both timeline layouts to the found feature (#5850)

Find in Timeline selected the feature but neither the bottom History strip
nor the history on the left scrolled to it. The handler now leaves a one-shot
reveal request (TreeState::timeline_reveal); whichever layout draws its items
next scrolls the found item into the middle of its scroll area and drops it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:44 +00:00
raykholo 0343960bd6 Ignore middle-click clipboard paste during navigation (#5855) (#5924)
* claim: #5855

* Ignore middle-click clipboard paste during navigation (#5855)

Firefox on Linux sends a primary-selection paste event when the middle
button orbits or pans. Paste events on frames where the middle button is
pressed, held, or released no longer reach the design; Ctrl/Cmd+V still
pastes.
2026-10-25 01:25:40 +00:00
DuBearte 1d48707d0c Drive Joints: a Show ghost checkbox (#5905) (#5955)
* claim: #5905

* Drive Joints: a Show ghost checkbox (#5905)

Driving a joint draws the moved parts' old place as a translucent ghost,
which gets in the way when driving joints over and over. Drive Joints has
a Show ghost box under its value rows: on by default (nothing changes),
off draws only the moved parts at their new place. The stopped-short
warning stays. The choice is a preference (drive_joints_ghost), so it is
remembered between uses; it affects Drive Joints only.

The preview's Colors carry the choice (ghost), so toggling it recomputes
the preview (its key hashes the colours) and world_preview leaves the
ghost out.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:38 +00:00
DuBearte 443baa41f0 Design tabs: the tooltip says where the file is saved (#5868) (#5974)
* claim: #5868

* Design tabs: the tooltip says where the file is saved (#5868)

Hovering a design tab showed the design's name, the same text as the tab.
The tooltip now says where the file is saved, with a path in the home
folder shortened to ~/ (home::friendly_path, as the start page does), or
"Not saved yet" for a design never saved. When the tab is too narrow for
its name, the full name comes first, then the place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:36 +00:00
Colin Faulkingham d366b8922a Keep the main sketch tool unchanged when a pinned tool starts (#4023) (#6013)
* claim: #4023

* fix(ui): preserve sketch family choices when starting pinned tools
2026-10-25 01:25:33 +00:00
Colin Faulkingham 692265980b Identify commands beside the animated agent cursor (#622) (#6026)
* claim: #622

* fix(ui): identify commands during agent cursor animation
2026-10-25 01:25:31 +00:00
Colin Faulkingham 28fe5fdb43 Extrude: accept manually selected target planes (#4974) (#6030)
* claim: #4974

* fix(ui): accept manual extrude plane targets (#4974)
2026-10-25 01:25:29 +00:00
semmelrinde 26c9d1c445 View toolbar: the Look At tooltip names its shortcut, Ctrl+8 (#2476) (#6071)
Look At already has a key (Ctrl+8, Cmd+8 on a Mac), but the button's
tooltip didn't say so, as Fit's does (F6).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:24 +00:00
semmelrinde f157f5489f test: the desktop-PATH agent probe waits long enough on a loaded machine (#6072)
desktop_path_can_start_an_agent_and_its_runtime gave the version probe
3 s and the first reply 5 s. Under a full test run on a loaded macOS
machine the probe came back empty (version None, twice in a row), while
the test alone passes in 0.4 s. Both limits are now 20 s; a working
agent still answers at once, so the test takes no longer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:22 +00:00
semmelrinde d1ec8dfd5a Agent chat: a Mammouth AI preset for OpenAI-compatible endpoints (#5815) (#6082)
* Agent chat: a Mammouth AI preset for OpenAI-compatible endpoints (#5815)

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

* Agent chat: the key test names the Mammouth AI URL too (#5815)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:19 +00:00
semmelrinde 08d094db39 Shell: an empty Thickness makes no command, so OK waits for one (#960) (#6085)
With the Thickness box cleared, the dialog sent thickness: "" and the
command refused it ("`thickness` must be a number or an expression"),
shown as a red error with Create Issue. The dialog now makes no command
until a thickness is entered.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:17 +00:00
github-actions[bot] 5fa6361ff2 Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci] 2026-10-25 02:19:39 +00:00
Colin Faulkingham 1f93b6d39c Joint: use the picked straight edge as the motion axis (#5457) (#6086)
* claim: #5457

* fix: align new straight-edge joint snaps with the edge (#5457)
2026-10-25 01:25:14 +00:00
semmelrinde 0477e0ba4e Shell: the dialog sends the picked face's body (#4555) (#6092)
Where a body's face lies flush with another body's face, the face point
alone could resolve to the other body, and Shell then refused with
"select at least one face to remove". The dialog now passes the body of
the picked faces, as the command already accepts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:12 +00:00
semmelrinde ae7176cc86 Extrude: a face picked under a picked profile is extruded too (#3926) (#6099)
With a sketch profile and a body face picked together, OK ran the
profile's extrude first and then looked the face up by its point. Where
the profile's extrude covered the face, it was gone: "no face of Body1
at face". A new extrude now runs its face picks first; an edit still
rebuilds the profile's extrude first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:09 +00:00
semmelrinde eef9a68809 Dialogs: a missing pick on OK is guidance, without Create Issue (#3292) (#6101)
Plane at Angle's OK with an empty Plane or Axis input showed "select a
plane first" or "pick an axis" as a red error with a Create Issue
button, and people filed it as a bug (#3292, #3773). A refusal that only
names what to pick is now shown as a plain hint, with no issue button,
in every dialog (Sketch Modify dialogs already hid it).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:06 +00:00
Colin Faulkingham f06875128b Preserve the history marker when switching sketch editors (#5435) (#6103)
* claim: #5435

* fix(ui): preserve history when switching sketch editors
2026-10-25 01:25:04 +00:00
semmelrinde 9d226b3535 Sheet metal DXF: options to leave out the bend lines and to use one layer (#4314) (#6130)
sheet.export_dxf always wrote the dashed bend lines (BEND_UP,
BEND_DOWN) and split the flat pattern over a layer for each role. A
cutter that cuts every line it is given cut the bend lines too.
bend_lines: false now leaves them out, and layers: single writes every
entity on layer 0. The defaults keep today's output.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:25:02 +00:00
Colin Faulkingham 7a1f903216 Close the File menu with Escape (#6126) (#6132)
* claim: #6126

* fix: close File menu on Escape (#6126)
2026-10-25 01:25:00 +00:00
NOWN-DEV b88673f366 Model cache: each sheet body is written once, not with every state (#6152)
* Test: a saved model keeps each sheet body once

A sheet-metal design wrote its sheet bodies again with every later feature's state in the saved
model: each feature that leaves the sheet alone (a box beside it) adds more than the whole sheet
to the records. A design of many features after its sheet metal passed the cache's 32 MiB
manifest limit that way and kept only some of its states (#3220). The test also opens the saved
file and wants the sheets taken from it as evaluated.

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

* Model cache: each sheet body is written once, not with every state

Problem: a saved model writes every feature's state, and each state wrote its sheet bodies in
full; most features leave the sheets as they were, so a sheet-metal design of many features
repeated them over and over (a 280-feature part: 30.5 MB of manifest, 29.6 MB of it 69 distinct
sheet records written 243 times). Past the manifest's 32 MiB limit the save thinned the cache
(#3220) and opening evaluated the left-out features again, although the distinct data was small.

Solution: the records keep a list of sheet bodies, each written once; a state names its sheets by
their place in it, as it already does for bodies and sketches. A thinned model keeps the whole
list and counts it once against its budget; the reader counts sheets with the bodies and sketches
a state holds; the cache format is now /4 (an older cache is a miss).

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-25 01:24:57 +00:00
DuBearte 67adf5abb5 Preferences: the theme strip shows arrows where more themes are (#6161) (#6167)
* claim: #6161

* Preferences: the theme strip shows arrows where more themes are (#6161)

The strip scrolls sideways with a hidden scrollbar, so nothing showed that
more themes were past its end. An end with themes past it now shows a
chevron button over the tiles there; a click scrolls most of a page.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:54 +00:00
Dahak Builds adbd2adf89 view.pick: map screenshot pixels to exact geometry (#6304) (#6329)
* claim: #6304

* view.pick: map screenshot pixels to exact geometry (#6304)

A read-only command that answers, for screenshot pixels (window or model
frame) or world-space rays: body, face index, exact world point and
outward normal, depth, occurrence/component for placed parts, and the
nearest edge/vertex within edge_px. Faces are raycast through the same
per-body pick index the mouse uses; flat faces are solved exactly
against their plane, curved faces take the mesh hit with the normal
interpolated from the mesh's surface normals. Section-aware: faces on
the removed side do not stop a pick and the cut cap answers with
section_cap. Never touches the selection, camera, document or undo.

Exposed as control ui.pick and as view.pick through engine.execute and
engine.batch (and through them MCP execute and batch).

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-25 01:24:39 +00:00
Dahak Builds 96303873fe Sketch: right-click toggle between driving and driven dimensions (#320) (#6357)
* claim: #320

* Sketch: toggle a selected dimension between driving and driven (#320)

A selected sketch dimension now offers 'Toggle Driven Dimension' in the
viewport right-click menu, running the existing engine command
sketch.toggle_driven. The over-constrain -> driven placement half landed
in #366; this exposes the engine's toggle in the UI, the remaining gap
from the retest notes on #320.

- context_menu: sketch_entity_items collects selected SketchConstraint
  ids that are dimensions (kind.value() is Some) and, for a single
  selected dimension, prepends the toggle item aimed at that constraint.
  Non-dimension constraints and plain entities get no item.
- Label reuses the existing catalog entry cmd.sketch_toggle_driven, so
  every locale translates it with no catalog change.
- Test: the menu offers the item for a selected dimension, running it
  round-trips driving -> driven -> driving, and a horizontal constraint
  or a plain curve gets no item.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-25 01:24:36 +00:00
NOWN-DEV b925a1af3d Configurations: a long table builds only the rows on screen (#6366)
* Test: a long configuration table builds the cells of the rows on screen

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

* Configurations: the table builds the rows on screen and its cells once per change

A table of hundreds of configurations was slow to open and to use: every frame cloned the
whole config.table answer, rebuilt every cell's text, looked each parameter's unit up once
per cell and built an editor for every cell of every row, on screen or not.

The answer and its cells (with units, looked up once per column) are now kept per design
revision, and rows off screen keep their space without building their cells, as the
parameters table already does. A row with an edit in progress stays live, so leaving its
cell still commits the edit.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:33 +00:00
Matthew Knutson 05d43d3542 Windows: the taskbar button shows the mark when the first frames are slow (#6391)
The taskbar asks a window for its icon once, when the window appears. eframe
sets the icon while the window is still hidden and shows it after the first
frame; when that frame is slow (safe mode's software renderer) or startup kept
the UI thread busy long enough for Windows to mark the window as not
responding (building the viewport pipelines), the request isn't answered in
time, the taskbar falls back to the window class's icon, and winit's class has
none: the button showed Windows' generic icon for as long as the window was
open.

Give the class the exe's embedded icon (resource 1) before the window is shown.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:24:26 +00:00
NOWN-DEV c0640cb95e inspect.parameter_sweep: check a design across a parameter's range (#2273) (#2276)
* inspect.parameter_sweep: check a design across a parameter's range

Rebuilds a copy of the design at each value of a parameter and reports what
no longer holds compared with the design as it is: features that fail or
warn, joint origins that are lost or turn over on their own part, sketch
planes that turn over, and sketch solves that fail or fold. The design is
not changed; each value's rebuild time is reported too.

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

* inspect.parameter_sweep: params doc matches what it takes and returns

`steps` takes at most 199 (200 values), as the check says; failed and
warned entries carry the feature's name; a lost origin says which one; a
value the parameter refuses gives {value, ms, ok: false, error}. The
refusal test covers steps 200.

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

* inspect.parameter_sweep: Japanese and Chinese labels

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 01:19:00 +00:00
Tyler Chiriboga 2ff17c0fcb 3D mouse: follow the device that moves, not the first one found (#3988) (#4339)
* claim: #3988 (3D mouse: follow the device that moves, not the first one found)

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

* 3D mouse: follow the device that moves, not the first one found (#3988)

On Windows and macOS the reader opened the first 3D-mouse interface it
could and read it until it failed. A 3Dconnexion Universal Receiver with
nothing paired opens fine, has a valid layout and never fails, so it kept
the reader for good. A SpaceMouse on a cable next to it was never read
("Not calibrated (hid 256f:c652)", every axis at zero).

The reader now opens every candidate and follows the one in use. The
first is followed from the start, as before. Another takes over once it
shows use (an axis off zero or a button down) while the followed one has
shown none for STALE_AFTER, so two devices moved together don't trade
the reading. A candidate that fails is dropped; if it was the followed
one the reading resets and the next is read. With one device the reader
behaves as before, down to the 100 ms read timeout. Several take 10 ms
turns, and reports queue in the system meanwhile.

The selection is `follow()`, written against a small `ReportSource`
trait, so it is tested with simulated devices on every platform: an idle
receiver next to a SpaceMouse, two devices in use at once, and devices
failing.

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

* 3D mouse: a press that makes a device the one in use counts as a press

Since #3825 a HID device's first button report after it connects is its
baseline: a button down in it is held, not pressed. A device taking over
from another one was connected at that moment, so the press that took
over was swallowed. With an idle Universal Receiver found first, that is
the first button press on the SpaceMouse.

follow() now keeps the buttons each candidate last reported while another
was read, and the reader hands them to the reading as the baseline when
that candidate takes over. A device that reported no buttons before it
took over keeps the held-not-pressed rule.

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

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:18:34 +00:00
Victor Santos Oliveira ecf57d131a Make the generated command reference easier to browse (#5237) (#5721)
* claim: #5237

* docs: make the generated command reference easier to browse (#5237)
2026-10-25 01:17:36 +00:00
N3MI-DG 32ad6e0832 Command search: the closed field draws with the ribbon, under system windows (#5804)
The ribbon's command-search field had its own area at `Order::Foreground`, so
it painted above every system window: a Preferences card moved under the top
right corner showed the search field floating on top of it, while the rest of
the ribbon went under the card as it should.

Closed, the field is ribbon chrome and now draws at `Order::Background` with
the rest of the ribbon; open (or animating open or shut), it keeps
`Order::Foreground` so the results sheet still floats over every window.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-25 01:17:30 +00:00
Matt Ruhland 13c4a62a71 fix(skills): correct statements that no longer match 0.21.0 (#5899)
On 0.21.0 the facts test failed on rectangle symmetry, which now works
on two sides. Other statements in the modeling and mechanisms skills
also no longer match: edge picks have a distance limit, concentric
circles extrude as a ring, a hole sketch below a part drills into it,
slot ids count on from earlier curves, shell refuses curved blends and
lofts, measure and inspect_design grow with the body count, and
joint.sweep needs occurrences with against.

Each correction has a test in test_facts.py.
2026-10-25 01:17:23 +00:00
bherbruck bba1953623 Fillet/chamfer: find each edge on its own body (#6378)
* Fillet/chamfer: find each edge on its own body

A blend picked over edges of several bodies was evaluated on one body,
the one nearest the first pick, and every edge was named on that body
when the feature was made. Picks on the other bodies were named after
that body's nearest edge and rounded it instead (or moved to it by
position once an upstream edit moved them). With two stacked boxes, a
height change put the upper box's picks on the lower box's rounded
corners: the upper box's fillet resolved there and its own edges lost
their rounds.

Now each edge is named on the body it was picked on, and evaluation
finds each edge's body by its stored name (a gone name stays on the
feature's other bodies), groups the edges by body and blends each body.
A name an older build recorded on the wrong body (the pick lies outside
the box its reference recorded) is not used; the pick finds the edge.
A pick an upstream edit left at an end of its named edge goes to the
edge's middle, so the kernel cannot take the neighbouring edge.

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

* Fillet/chamfer: skip the per-body lookup when there is one body; name each body's picks in one pass

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 00:57:53 +00:00
fugazister 4b119dd471 Performance overlay: frame-time graph and histogram, triangles, draw calls (#2548) (#2549)
* Performance overlay: frame-time graph and histogram, triangles, draw calls (#2548)

Net change of the perf-overlay branch, rebased onto main as one commit.

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

* Document frame_rate_details in ui.prefs

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-24 22:50:43 +00:00
raykholo 6224cbf526 Thread: the refusal for a fine-only size says what to type (#4971) (#4977)
* claim: #4971

* Thread: the refusal for a fine-only size says what to type (#4971)

M28 and other sizes without an ISO coarse pitch were refused as an
unknown thread, with examples of other sizes. Thread and threaded Hole
now say that the size has no coarse pitch and suggest two to give, such
as M28x2 or M28x1.5. The coarse table is unchanged.

* Thread: automatic sizing past M56 says what to type (#5055)

Automatic thread sizing only knows the ISO coarse sizes up to M56, so a
75 mm shaft was refused with "no metric thread fits a 75.00 mm
cylinder", although M75x2 and M75x3 work when typed. Past the end of
the coarse range, the refusal now says so and suggests two sizes with
their pitch, scaled to the diameter (a hole's size adds one pitch to its
bore). The coarse table is unchanged.
2026-10-24 22:49:52 +00:00
dudedagger42 e89a8f56c1 MidPoint: the point goes to the line's middle, whichever is picked first (#5011)
MidPoint on a point and a line keeps the first pick still and moves the second.
With the point picked first (an end of one line, then the line it should sit in
the middle of), the line turned about its far end to bring its middle to the
point: a line from (0,0) to (20,0) went to (0,0)-(10,10) for a point at (5,5).
A point and a line now put the line first, so the point moves and the line
stays; the line still comes to the point when the point can't move.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:49:49 +00:00
raykholo 4cd1a51f48 Assistant: a request timeout per endpoint for slow models (#5009) (#5010)
* claim: #5009

* Assistant: a request timeout per endpoint for slow models (#5009)

An OpenAI-compatible endpoint now stores how long one model request may
take (request_timeout_s, 300 s by default, clamped to 30 s ..= 1 h). It
is set in the Add agent dialog and in the endpoint's entry under Agents
(a running conversation uses a change from its next request), through
ui.agent_chat add_agent, and reported in the chat's state. A request
that runs out of time says the limit and where to raise it instead of
"timeout: global". The desktop HTTP cap goes from 900 s to an hour.

The tool-call timeout stays 330 s: it covers SolidCraft's own tool work
and the user's Allow, never the model.

* Assistant settings: line up an endpoint's Request timeout with Permissions

The label column of an agent's entry is wide enough for "Request timeout",
so the label isn't cut and both rows share one column.
2026-10-24 22:24:42 +00:00
raykholo 88f079bd8c Timeline: the tooltip hint is translated (Part of #4370) (#4398)
* claim: #4370

* Timeline: the tooltip hint is translated (Part of #4370)
2026-10-24 22:12:28 +00:00
imanu86 dbdeeec2e9 Undo restores what a hide put away (#2219) (#4780)
* Undo: what is hidden lives in the snapshot, so hiding is a step (#2219)

`Session::execute` records a step only when the document `Arc` changes, and what is
hidden lives in `Session::visibility` — saved with the design, but outside the
document. `browser.visibility` was also declared `.noundo()`, and `Snapshot` had no
field for it, so even a recorded step could not put a hidden body back: undo went to
the command before the hide and left it hidden (#2219).

`Snapshot` and `Checkpoint` now carry the `Visibility`, `go_to` restores it, and
`Session::execute` treats a visibility change as a change of its own. That change is
display-only, so it refreshes the view with a revision bump (`refresh_display`)
instead of evaluating a model a hide does not touch.

The command also takes `folder: "all"` (every body, sketch, construction item, origin
item and joint, plus the browser folders and occurrence hides) so Show All can be one
step, and asking for the state an item already has no longer reorders the hidden
lists: a redundant hide is not a step and leaves no unsaved changes.

* Show/Hide: the menus run browser.visibility, so the history has it (#2219)

The hide and show actions wrote `session.visibility` directly instead of running the
command: the body and face menus (`ui.hide` / `ui.show`), the `V` key
(`context_menu::toggle_visibility`), the browser rows' eyes, the Origin rows and
`ui.originAll` / `ui.originPart`. The engine could record a step and there was
nothing to record — which is why the release still answered "nothing to undo" after
hiding from the Browser.

They all run `browser.visibility` now (the repo rule that the UI acts through
commands), and `ui.showAll` is that one `folder: "all"` call, so Show All is one undo
step rather than three. `docs/occurrence-workflows.md` says what visibility does in
the history.
2026-10-24 22:12:17 +00:00
Sain2d 1dbb13cad2 Korean: translate the 297 messages added since the last pass (#5147)
assets/locales/ko.json had 298 ids that en.json has and it lacks (the
help.solid.* texts, the whole Preferences page, the issue-report dialog,
history names, ribs, mouse gestures and navigation presets). Add Korean for
297 of them; "Of" is left untranslated because its context is unclear.

Wording follows the existing ko.json (필렛, 모따기, 돌출, 쉘, 바디, 피처,
비활성화, 환경 설정, 어시스턴트, 에이전트). The file stays in the canonical
sorted one-message-per-line form; no existing message changed.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 22:11:59 +00:00
raykholo 0b91ff8a34 Measure picks projected and intersection sketch curves (#4337) (#4427)
* claim: #4337

* Measure picks projected and intersection sketch curves (#4337)

Dialog inputs turned away every curve or point of a sketch that is not the
active one, which includes projected and intersection curves, so Measure
took the nearest item it did accept instead. Give Measure explicit
acceptance for these references, keep the source sketch on a click, and
accept them from a pre-selection and a box selection. Other inputs keep
their restrictions.
2026-10-24 22:05:58 +00:00
raykholo 1e81e65e7e Browser tree hover stays fast on a nested STEP assembly (#3892) (#3894)
* claim: #3892

* Browser tree hover stays fast on a nested STEP assembly (#3892)

Hovering a placement row lit its whole subtree by walking it again
(Session::occurrence_bodies) on every frame, and opening a STEP file
makes its top placement active, so every highlight rebuild walked the
whole assembly again for the active placement's bodies. On a nested
assembly each walk resolves every placement below it, which cost
0.5 s or more per walk.

- Session::occurrence_bodies remembers its answers per placement until
  the document, the evaluated model or the pending moves change
  (occurrence_bodies_shared hands out the shared list).
- The highlight looks hovered bodies and faces up by name instead of
  scanning the hovered list once per body.
2026-10-24 21:59:55 +00:00
Christian Svensson 9227685483 Smooth sketch dragging on large imported faces (#3222) (#3480)
* claim: #3222

* Sketch drag: project a face sketch's links once per live gesture (#3222)

Every live sketch.drag frame refreshed the sketch's links before solving
the drag. For a sketch on a face that projects all of the face's edges
again: 115-460 ms per frame on the top face of a 600 x 500 mm plate with
120 holes imported from STEP, against about 1 ms for the drag's own solve.
The model the links come from cannot change during the gesture.

The session now remembers the sketch, document and revision a live frame
left. The gesture's next frame reuses the links while nothing else has
run: any other command (undo included) or any other change to the design
makes the next frame project them again. A test counts the projections.

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

* Sketch drag test: drop an unread assignment (#3222)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:53:32 +00:00
N3MI-DG 582e6097be Find in Browser finds the body; Find in Timeline opens the feature's folders (#3883) (#3884)
* claim: #3883

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

* Find in Browser finds the body; Find in Timeline opens the feature's folders (#3883)

A right-click on a body in the viewport picks a face, and the face menu's
Find in Browser selected the face's creating feature in the Features folder
instead of the body. It now reveals and selects the body, as the edge and
body menus already did.

Find in Timeline now opens any collapsed history folders around the feature,
and opens the Browser's Features folder for the picked occurrence, scrolled
to the feature (the reveal formerly behind Find in Browser).

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

* Find in Browser opens collapsed rows above the body and finds later placements (#3883)

Open the document, placement, Bodies and group rows when the command runs, not while the
Bodies folder is drawn, so a collapsed ancestor no longer hides the body. Match the scroll
target by the placed body's world name, and fix the group row key ({scope}/g{id}).

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

* Find in Browser picks the body's own placement; Find in Timeline leaves the Browser alone from rows (#3883)

Every component's first placement keeps the definition's body name, so the root placement
matched before the body's own one and Find in Browser on a first copy opened nothing above it:
the placement must belong to the body's component too.

Find in Timeline from a sketch, construction plane or feature row has no body to say which copy
was meant, so it opened the Features folder of whichever placement came first (the other copy),
or one that never lists sketches. From those rows it now only reveals the feature in the
History; from a face, edge or body it still opens that body's copy's Features folder.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:53:27 +00:00
raykholo 22190e703c Revolve, sweep, loft and hole from a nested STEP part's face join and drill it (#3223) (#3987)
With an opened STEP assembly's imported root active (it owns no body), a
Join revolve, sweep or loft from a sketch on a nested part's face made a
separate body, and a hole at the points of such a sketch found no body to
cut. A Join with no targets now takes the body its profile sketch is on,
through the same active scope as an extrude, and a hole counts as a cut
for the rule that a cut from a component with no body works on every body.

Refs #3223
2026-10-24 21:53:23 +00:00
leValdemar19 f7b08ab8c4 Dimension values: #name = value defines a parameter too (#2371) (#4212)
* claim: #2371

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

* Dimension values: `#name = value` defines a parameter too (#2371)

`#width = 40` in a dimension's value now works like `width = 40` (#2916):
it adds the user parameter `width` and dimensions with it. On `main` the
`#` made it an ordinary expression that failed to parse.

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-24 21:53:16 +00:00
TheMiddleCompany ae0b1edeac Coil: a section from a sketch profile (#4236) (#4239)
* claim: #4236

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

* Coil: store an optional sketch and profile selection

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

* Coil: sweep a sketch profile along the helix

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

* Coil: accept a sketch and profile selection

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

* Coil dialog: pick a sketch profile for the section

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

* Coil: cargo fmt

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

* Coil: refuse a loaded sketch coil that also sets the primitive section

A saved design could add section, section_position or start_angle to a
coil that already has a sketch; eval ignored them instead of refusing the
contradiction as the command does.

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

* Coil: assert the kernel refusal and exercise hostile profile selections

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

* Coil dialog: keep the sketch-profile check after rebase

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

* Format

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-24 21:53:13 +00:00
Nathalie Schönwetter cf1b0d67cc Extrude by pressing on a face or profile and pulling (#4767)
* feat: extrude a face or profile by pressing on it and pulling

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

* feat: pull-to-extrude keeps the window open and sets the step by sideways motion

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

* Pull to extrude: coarser step changes, a step ladder below the pointer, the step in the distance box

The step label sat on top of the distance box at the arrow tip. It is now a ladder of the
step in use between its neighbours, below the pointer, and the distance box also names the
step ("Step 1 mm"). One step change takes 44 px of sideways travel instead of 22.

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

* Pull to extrude: a setting, off by default (Preferences > Navigation)

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

* Rename the pull setting to Direct interaction

It will also cover other direct gestures in the viewport later (grabbing an edge to round it).

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

---------

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:52:58 +00:00
Nathalie Schönwetter b702ce8c7d fix: start the window on Intel Macs without wgpu's indirect-call validation (#3024) (#4808)
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:52:55 +00:00
raykholo 60105172b2 Sketch on a turned occurrence: reuse moved bodies across edits (#4853) (#4863)
* claim: #4853

* Sketch on a turned occurrence: reuse moved bodies across edits (#4853)

A sketch with a face anchor is evaluated in its component's frame.
state_in_frame_at moved every body placed differently from that frame
with a fresh transform on every evaluation, and the face-anchor frame
moved the anchored body again, so each sketch edit on a turned or moved
part transformed and re-meshed it.

The model now keeps the bodies it moved into a feature's frame, keyed on
the source body and the exact matrix (and checked against the source's
mesh, geometry, colour, paint and split lines, as world placement does).
An unchanged body under an unchanged placement is moved and meshed once.
Each evaluation starts a new generation and drops entries that neither
it nor the one before used, so the cache holds only placements in use.
Model states hold a weak reference, so the cache never keeps earlier
states alive.

Fixes #4853
2026-10-24 21:52:52 +00:00
Nathalie Schönwetter e2b8bf5a05 fix: copy a pattern's features in timeline order, not in the order they were picked (#4873)
A circular or rectangular pattern of a hole and the chamfers on its edges failed with
"no edge near ..." when a chamfer was picked before the hole: the chamfer was copied
first, onto a body that had no hole there yet.

Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:52:49 +00:00
dudedagger42 9e065aa0d5 Viewport: keep the sketches' box per world state, not every frame (#5223)
projection() reads the model's box for the scene's depth, several times a
frame, and model_bounds() sampled every sketch's curves for it each time,
hidden sketches too. A 533-feature design with 445 sketches spent about 45 ms
of every frame there (stack samples: model_bounds -> Sketch::bounds -> segs ->
spline polylines), so orbiting ran near 16 fps. The sketches' box is now kept
for the world state it was made from, as the bodies' boxes already are.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:39:33 +00:00
raykholo 2fd7b6f08d Measure: a face or edge picked by its point alone (#4099) (#4114)
* claim: #4099

* Measure: a face or edge picked by its point alone (#4099)

inspect.measure (and the MCP measure tool) refused a face or edge item
without an index ("missing field `index`"). Face and edge items are now
read as body + point with an optional index: the point finds the face
(the same lookup face picking uses) or the nearest edge within the pick
tolerance, before the selection is built. A missing body or a point on
no face or edge is a clear error. Explicit indices still work.
2026-10-24 21:27:36 +00:00
raykholo 59795872f6 Extrude: document which way a positive taper goes (#4098) (#4115)
* claim: #4098

* Extrude: document which way a positive taper goes (#4098)

A positive taper slopes the walls outward (the far end is larger along the
extrusion direction), a negative one inward. Say so in the params doc, the
JSON schema and the guide, and pin the sign with a test.
2026-10-24 21:27:34 +00:00
Alexander Puzynia 37b7b0bd3e Offset free-form curves and mixed chains: trim folds, round corners (#2149, #2676, #2761, #3159, #3828) (#4156)
* Offset of free-form curves: trim folds, round chain corners

Offsetting a spline by more than its tightest bend failed with "offset spline
has no interior certificate correspondence": the plain offset folds over itself
there and a single linked spline cannot follow it. A chain of Bézier curves (any
imported SVG outline) was refused outright.

Such an offset is now the parallel boundary: the plain offset of every piece,
joined round each corner by an arc about it, trimmed where it folds or crosses
itself (a sharp corner is left), each kept run fitted with an open spline and
certified to the offset tolerance, and the whole proven free of crossings. Tiny
kinks are joined directly; zero-length Bézier handles (infinite curvature at
the end) are replaced by their chord within a quarter of the tolerance. A
single curve whose plain offset is smooth still gets the linked offset; the
trimmed or chained result is ordinary curves, not linked to the source, so the
command adds no distance parameter for it.

* Offset chains mixing lines or arcs with free-form curves

A chain offset read every curve with Sketch::exact_segs, which only covers free-form
curves, so a chain with straight or circular pieces (a text glyph, an SVG path with
L commands) failed with "offset source changed shape". Read them with exact_pieces,
which gives lines and arcs from their analytic form; the trimming already handles
those pieces.
2026-10-24 21:27:31 +00:00
dudedagger42 7e24f0e35a Sketch: a group too large to solve says so, not "conflicts with" nothing (#5280) (#5347)
The solver takes at most 4000 unknowns in one connected group, and a
pattern joins all its copies into one, so a hexagon patterned 17 × 20
(4000+ unknowns) failed as "that would over-constrain the sketch
(conflicts with )", naming no constraint. The solve report now carries
the size of the largest group it could not take, and the failure says
how many unknowns are joined, the limit, and that fewer copies or
several patterns or sketches stay within it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:26:40 +00:00
bherbruck 152efb5774 Windows installer: add the install folder to the system PATH (#6340)
MCP clients configured with "command": "solidcraft-cli" could not find it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:26:33 +00:00
rlartot e726f780d1 Sketch: the first dimension scales only a connected drawing (#3786) (#3815)
* claim: #3786

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

* Sketch: the first dimension scales only a connected drawing (#3786)

The first length dimension scaled the whole sketch about its origin, so a
second rectangle that nothing links to the first was resized and moved too.
Now the sketch scales only when its curves form one chain, joined by shared
points or constraints (the origin counts as a point); separate shapes keep
the previous behaviour: the dimension sizes the shape it measures.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:21:08 +00:00
Dennis Klappe b201c283c7 Model-cache tests trust an unknown source commit, so they pass without git (#4221)
Fixes #4045
2026-10-24 21:20:56 +00:00
dudedagger42 a2ae3ffbe0 Sketch drag: keep the far end of a tangent line still (#4925)
Dragging one end of a line tangent to a circle or arc let the solver move the
line's other end too, sometimes far ("goes flying"): only the dragged point was
held, and the least-squares step spread the change over the free points. The
drag now first tries holding the line's far end as well, and falls back to the
old solve only when that can't be met. Dragging the end of a tangent line to
(5, 12) moved its other end 0.399 mm on 0.19.0; it now stays put.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:20:15 +00:00
Dennis Klappe 96661111f9 OBJ export shares vertices and keeps body names as written (#4462)
Fixes #4451
2026-10-24 21:13:49 +00:00
571 changed files with 46260 additions and 4437 deletions
+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
+6
View File
@@ -2,6 +2,12 @@
Describe the problem and resulting behavior. Link the issue when applicable.
Changes a default: no
<!-- Required line, replace "no" if this changes what a user gets out of the box (a default value, key binding, setting, tool behaviour):
Changes a default: yes — <what>, old behaviour kept behind preference <name>
Docs-only and bot PRs are exempt. -->
## Validation
What did you run, and what were the results? Include expected and measured values for geometry changes.
+1 -1
View File
@@ -295,7 +295,7 @@ jobs:
- name: Merge-of-main detection self-test
run: python3 -I scripts/ci/test_main_merge.py
- name: Claim, triage and kernel-paused bot tests
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js scripts/pr-checks/*.test.js
# Fast feedback for a pull request that has not been approved for the full CI yet.
quick-compile:
+9 -3
View File
@@ -1,6 +1,8 @@
# Files generated from the code are rewritten here, on main, after each merge, so pull requests
# never edit them (two pull requests that each regenerated one would always conflict):
# THIRD-PARTY-LICENSES.txt (`cargo xtask licences`) and docs/features.md (`cargo xtask features`).
# Pending files (assets/locales/pending, attribution/pending) are folded into the catalogs and
# ATTRIBUTION.md by `cargo xtask fold-pending`.
name: Generated files
on:
@@ -17,6 +19,8 @@ on:
- "crates/engine/**"
- "apps/solidcraft-cli/**"
- "xtask/**"
- "assets/locales/pending/*.json"
- "attribution/pending/*.md"
- ".github/workflows/generated.yml"
workflow_dispatch:
@@ -48,13 +52,15 @@ jobs:
run: cargo xtask licences
- name: docs/features.md
run: cargo xtask features
- name: Pending locale strings and attribution rows
run: cargo xtask fold-pending
- name: Commit if changed
run: |
if git diff --quiet -- THIRD-PARTY-LICENSES.txt docs/features.md; then echo "unchanged"; exit 0; fi
if [ -z "$(git status --porcelain)" ]; then echo "unchanged"; exit 0; fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add THIRD-PARTY-LICENSES.txt docs/features.md
git commit -m "Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]"
git add -A THIRD-PARTY-LICENSES.txt docs/features.md assets/locales ATTRIBUTION.md attribution
git commit -m "Regenerate generated files and fold pending files [skip ci]"
for _ in 1 2 3; do
git pull --rebase origin main && git push origin HEAD:main && exit 0
sleep 10
+2
View File
@@ -39,6 +39,8 @@ jobs:
run: python3 packaging/tests/nightly_test.py
- name: Windows executable icon validation regression
run: python3 packaging/tests/windows_icon_test.py
- name: AppImage CLI wrapper regression
run: python3 packaging/tests/apprun_test.py
- name: Flatpak desktop smoke validation regression
run: python3 packaging/tests/flatpak_test.py
- name: PowerShell
+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
+69
View File
@@ -0,0 +1,69 @@
# Two quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
# for an issue a lower-numbered one already closes, and the "Changes a default:" line is filled in.
# Neither is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
name: PR checks
on:
# pull_request, never pull_request_target: nothing here checks out or runs the PR's code, and the
# token is read-only for fork PRs. Only scripts/pr-checks from the PR's own merge commit is run.
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
concurrency:
group: pr-checks-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
duplicate:
name: Duplicate PR check
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
const r = await checks.checkDuplicate({ github, context, core });
core.info(JSON.stringify(r));
defaults:
name: Defaults line check
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
const r = await checks.checkDefaults({ github, context, core });
core.info(JSON.stringify(r));
# Labelling needs write access, which a fork PR's token never has, so it is its own job and best
# effort: a fork PR just doesn't get the `changes-default` label from here.
label:
name: Label changes-default
runs-on: ubuntu-24.04
timeout-minutes: 5
continue-on-error: true
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: scripts/pr-checks
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const checks = require('./scripts/pr-checks/pr-checks.js');
await checks.syncDefaultLabel({ github, context, core });
+63 -8
View File
@@ -4,11 +4,12 @@
# comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes
# 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. Nothing is signed yet.
# 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.
#
# 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
# macOS universal .dmg, and the CLI as a .zip packaging/macos
# macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) packaging/macos
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
# Web a .zip of the static site packaging/web
#
@@ -295,11 +296,19 @@ jobs:
path: dist/release/*
if-no-files-found: error
macos:
name: macOS universal
# The two halves of the universal binary build in parallel on their own runners (each alone
# took about half of the old single job's 45 minutes); `macos` below joins and packages them.
macos-build:
name: macOS build ${{ matrix.arch }}
needs: version
if: needs.version.outputs.build == 'true'
runs-on: macos-14
strategy:
fail-fast: false
matrix:
include:
- { arch: aarch64, target: aarch64-apple-darwin }
- { arch: x86_64, target: x86_64-apple-darwin }
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
@@ -307,12 +316,58 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-macos-universal
- name: Build and package (dmg, CLI zip; the dmg is mounted and checked)
run: packaging/macos/package.sh --arch universal
key: release-macos-${{ matrix.arch }}
- name: Build
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
# Not named release-*: the release job collects every release-* artifact as an asset.
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: macos-bin-${{ matrix.arch }}
path: |
target/${{ matrix.target }}/release/solidcraft
target/${{ matrix.target }}/release/solidcraft-cli
target/${{ matrix.target }}/release/solvecraft-cli
if-no-files-found: error
macos:
name: macOS universal
needs: [version, macos-build]
if: needs.version.outputs.build == 'true'
runs-on: macos-14
env:
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: macos-bin-aarch64
path: target/aarch64-apple-darwin/release
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: macos-bin-x86_64
path: target/x86_64-apple-darwin/release
- name: Set up Developer ID signing and notarization
# Optional until the APPLE_* secrets exist (docs/releasing.md, "macOS signing"). Without
# them (forks, dry runs) the app keeps its ad-hoc signature and is not notarized.
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: packaging/macos/signing.sh --setup
- name: Join, sign, package and check (dmg, CLI zip; the dmg is mounted and checked)
run: |
# Artifacts don't keep the executable bit.
chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli
packaging/macos/package.sh --arch universal --skip-build ${MACOS_SIGN_IDENTITY:+--sign}
- name: Remove the signing keychain and API key
if: always()
run: packaging/macos/signing.sh --cleanup
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-macos
+26 -11
View File
@@ -15,14 +15,18 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
`/solidcraft/`; do not add permanent old-name Pages aliases.
## 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.
1. What to work on: [ROADMAP.md](ROADMAP.md) (status and milestones), "Where help is wanted most"
in CONTRIBUTING.md, and the open issues labelled `help wanted`, `good first issue` and
`needs-triage`. Check what is already open first (below).
2. How the code fits together: [docs/architecture.md](docs/architecture.md) (the layer table) and
"How the code is laid out" in CONTRIBUTING.md. Kernel work is paused ("Kernel work is paused"
in CONTRIBUTING.md).
3. What we may depend on: the licence rules under Non-negotiables below, `deny.toml`, and
`cargo xtask licences --check`. Only `crates/kernel` may use `truck`.
4. Work on your own: when the code, the docs and these rules settle a question, act on it. Ask on
the issue or the PR only when the answer changes what users see or what the change covers.
`plan/` is gitignored (local only).
Maintainers may keep local notes in `plan/` (gitignored); nothing in this file depends on them.
## Never crash
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
@@ -35,7 +39,8 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
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 the Windows-only
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
@@ -81,7 +86,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
- **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,
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + input and output
schemas (a contract and a reply type in `crates/engine/src/schema/`, registered with
`checked!`, `.contract` and `.output`; a command should have them, and a test warns
without them (it will become required); `schema/unconverted.txt` only shrinks; how: docs/book/src/dev/schemas.md) + 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.
@@ -109,7 +117,9 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
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.
assets, layers). Commit after every feature arc that builds. The gate reads the working tree,
so do not switch branches while it runs: it compiles the other branch's code and fails on a
change that is not in the PR. Run it in the background and wait for its log.
- **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
@@ -155,6 +165,9 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
- 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,
@@ -205,7 +218,7 @@ before the draft is verified.
- 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
`docs/roadmap-detail.md` (committed) tracks status, milestones and estimates; `ROADMAP.md` is the short user-facing roadmap. 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.
@@ -214,3 +227,5 @@ workflow; never regenerate them in a PR.
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).
+15 -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 |
@@ -101,6 +101,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -157,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 |
@@ -198,6 +200,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
@@ -280,6 +284,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 |
@@ -512,6 +518,7 @@ 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 |
## Code adapted from sibling projects
+25 -3
View File
@@ -11,7 +11,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 +130,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
@@ -274,12 +276,18 @@ 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`
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
features`). Don't regenerate or edit them in a pull request.
- **Design format**: one file per change, never an edit to a shared number (above).
- **New UI strings and attribution rows**: one new file per change, `assets/locales/pending/<slug>.json`
and `attribution/pending/<slug>.md`, instead of editing the catalogs or the ATTRIBUTION.md table;
`cargo xtask fold-pending` folds them on `main` (see the README in each folder).
## Sending a change
@@ -331,6 +339,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
@@ -346,6 +364,10 @@ owning one area, with a human setting direction and reporting bugs. Their workin
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
long as the change meets the rules above.
Two quick checks run on every pull request (`.github/workflows/pr-checks.yml`, not required): it
fails when a lower-numbered open PR by someone else already closes the same issue (the label
`not-a-duplicate` skips it), or when the description lacks the `Changes a default: no` / `Changes a default: yes — <what>, old behaviour kept behind preference <name>` line (docs-only and bot PRs are exempt).
## Labels and triage
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
@@ -382,7 +404,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
+21 -17
View File
@@ -4208,7 +4208,7 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"block2 0.6.2",
"eframe",
@@ -4240,11 +4240,11 @@ dependencies = [
[[package]]
name = "solidcraft-3dx-client"
version = "0.22.0"
version = "0.23.1"
[[package]]
name = "solidcraft-cli"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"image",
"log",
@@ -4256,7 +4256,7 @@ dependencies = [
[[package]]
name = "solidcraft-derive"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4265,7 +4265,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"log",
"serde",
@@ -4282,7 +4282,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"log",
"schemars",
@@ -4296,12 +4296,13 @@ dependencies = [
"solidcraft-render",
"solidcraft-sketch",
"thiserror 2.0.21",
"toml_edit",
"web-time",
]
[[package]]
name = "solidcraft-geom"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"serde",
"serde_json",
@@ -4310,7 +4311,7 @@ dependencies = [
[[package]]
name = "solidcraft-gpu"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"image",
"log",
@@ -4330,13 +4331,14 @@ dependencies = [
[[package]]
name = "solidcraft-io"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"cadmpeg-core",
"cadmpeg-ir",
"cadmpeg-registry",
"chardetng",
"encoding_rs",
"gethostname",
"gltf",
"log",
"quick-xml",
@@ -4353,7 +4355,7 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"log",
"proptest",
@@ -4373,7 +4375,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -4382,7 +4384,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"image",
"moxcms",
@@ -4396,7 +4398,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"serde",
"serde_json",
@@ -4407,14 +4409,14 @@ dependencies = [
[[package]]
name = "solidcraft-space-mouse"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"serde",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"arboard",
"egui",
@@ -4443,7 +4445,7 @@ dependencies = [
[[package]]
name = "solidcraft-web"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"eframe",
"egui",
@@ -4452,6 +4454,7 @@ dependencies = [
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -4698,6 +4701,7 @@ dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
@@ -5979,7 +5983,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.22.0"
version = "0.23.1"
dependencies = [
"proc-macro2",
"serde_json",
+4 -1
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.22.0"
version = "0.23.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -67,6 +67,9 @@ encoding_rs = "0.8"
# glTF 2.0 import (MIT OR Apache-2.0): the document model and GLB container only; buffers are
# read by crates/io itself, so no image decoding or file access comes with it.
gltf = { version = "1.4", default-features = false, features = ["names"] }
# The computer's name in a design's "open elsewhere" lock file (MIT OR Apache-2.0; already in the
# tree through x11rb).
gethostname = "1"
# FreeCAD and Rhino import (Apache-2.0, pure Rust; native and browser builds).
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
+3 -3
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.
+80 -684
View File
@@ -1,686 +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 |
| 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. Trusted distribution 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
- 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 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)
+23 -5
View File
@@ -18,6 +18,7 @@
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
//!
@@ -82,7 +83,7 @@ const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
/// `mcp`'s lines in the usage.
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n";
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
/// `crashes`'s lines in the usage.
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
/// `licences`'s lines in the usage.
@@ -625,10 +626,23 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
Ok(())
}
/// `mcp setup`: add the server to each installed AI client, with this program's own path.
fn cmd_mcp_setup(args: &[String]) -> Result<(), String> {
use solidcraft_engine::agent_setup as setup;
let (lines, ok) = setup::setup(args, &setup::Env::from_process(), &setup::current_exe()?)?;
for l in lines {
println!("{l}");
}
if ok { Ok(()) } else { Err("mcp setup: some clients could not be configured".into()) }
}
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
fn cmd_mcp(args: &[String]) -> Result<(), String> {
if matches!(args.first().map(String::as_str), Some("setup" | "install")) {
return cmd_mcp_setup(&args[1..]);
}
let render_gpu = args.iter().any(|arg| arg == "--render-gpu");
if render_gpu && flag(args, "--connect").is_some() {
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
@@ -684,9 +698,8 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
Box::new(headless)
}
(None, None) => {
if !args.iter().any(|a| a == "--headless")
&& let Some(port) = discover_app()
{
let auto = !args.iter().any(|a| a == "--headless");
if auto && let Some(port) = discover_app() {
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
return serve_mcp(Box::new(solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token"))), scope);
}
@@ -695,7 +708,12 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
if render_gpu {
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
}
Box::new(headless)
if auto {
// The app may start after the agent: look for it again until there is work here.
Box::new(solidcraft_mcp::Auto::new(headless, Box::new(discover_app), flag(args, "--token")))
} else {
Box::new(headless)
}
}
};
serve_mcp(backend, scope)
+112
View File
@@ -151,3 +151,115 @@ fn mcp_without_arguments_finds_the_running_app() {
assert!(!String::from_utf8_lossy(&out.stderr).contains("bridged to the running app"), "{}", String::from_utf8_lossy(&out.stderr));
std::fs::remove_dir_all(&control_dir).ok();
}
/// `mcp setup` against a temporary home: the client folders it finds are the only ones it writes.
mod setup {
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::process::Command;
fn home(name: &str) -> PathBuf {
let h = std::env::temp_dir().join(format!("solidcraft-mcp-setup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&h);
std::fs::create_dir_all(&h).unwrap();
h
}
fn run(home: &Path, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["mcp", "setup"])
.args(args)
.env_clear()
.env("HOME", home)
.env("USERPROFILE", home)
.env("APPDATA", home.join("AppData/Roaming"))
.env("LOCALAPPDATA", home.join("AppData/Local"))
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("PATH", "")
.output()
.unwrap()
}
fn json_of(p: &Path) -> Value {
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
}
fn exe() -> String {
let p = Path::new(env!("CARGO_BIN_EXE_solidcraft-cli")).canonicalize().unwrap();
p.to_string_lossy().trim_start_matches(r"\\?\").to_owned()
}
#[test]
fn merges_into_opencode_and_cursor_keeping_other_servers() {
let h = home("merge");
let oc = h.join(".config/opencode");
std::fs::create_dir_all(&oc).unwrap();
let old = json!({"$schema": "https://opencode.ai/config.json", "theme": "x", "mcp": {"other": {"type": "local", "command": ["o"]}}});
std::fs::write(oc.join("opencode.json"), old.to_string()).unwrap();
std::fs::create_dir_all(h.join(".cursor")).unwrap();
std::fs::write(h.join(".cursor/mcp.json"), r#"{"mcpServers": {"other": {"command": "o"}}}"#).unwrap();
let out = run(&h, &["--live"]);
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let v = json_of(&oc.join("opencode.json"));
assert_eq!(v["theme"], "x");
assert_eq!(v["mcp"]["other"], old["mcp"]["other"]);
assert_eq!(v["mcp"]["solidcraft"], json!({"type": "local", "command": [exe(), "mcp"], "enabled": true}));
assert_eq!(v["mcp"]["solidcraft-live"]["command"], json!([exe(), "mcp", "--connect", "127.0.0.1:7878"]));
assert_eq!(json_of(&oc.join("opencode.json.bak")), old);
let c = json_of(&h.join(".cursor/mcp.json"));
assert_eq!(c["mcpServers"]["other"], json!({"command": "o"}));
assert_eq!(c["mcpServers"]["solidcraft"], json!({"command": exe(), "args": ["mcp"]}));
// Codex (TOML) and Claude Code (a big file the CLI owns) are edited in place too.
std::fs::create_dir_all(h.join(".codex")).unwrap();
std::fs::write(h.join(".codex/config.toml"), "# mine\nmodel = \"x\"\n").unwrap();
let cc = "{\n \"numStartups\": 3,\n \"projects\": {}\n}\n";
std::fs::write(h.join(".claude.json"), cc).unwrap();
assert!(run(&h, &[]).status.success());
let toml = std::fs::read_to_string(h.join(".codex/config.toml")).unwrap();
assert!(toml.starts_with("# mine\nmodel = \"x\"\n") && toml.contains("[mcp_servers.solidcraft]"), "{toml}");
let c = json_of(&h.join(".claude.json"));
assert_eq!(c["numStartups"], 3);
assert_eq!(c["mcpServers"]["solidcraft"], json!({"type": "stdio", "command": exe(), "args": ["mcp"]}));
assert_eq!(std::fs::read_to_string(h.join(".claude.json.bak")).unwrap(), cc);
// Running again updates the same entries and leaves one of each.
assert!(run(&h, &[]).status.success());
assert_eq!(json_of(&h.join(".cursor/mcp.json"))["mcpServers"].as_object().unwrap().len(), 3);
}
#[test]
fn dry_run_writes_nothing_and_missing_clients_are_skipped() {
let h = home("dry");
std::fs::create_dir_all(h.join(".cursor")).unwrap();
let out = run(&h, &["--dry-run"]);
assert!(out.status.success());
let text = String::from_utf8_lossy(&out.stdout);
assert!(text.contains("would write") && text.contains("solidcraft"), "{text}");
assert!(!h.join(".cursor/mcp.json").exists());
assert!(!h.join(".config/opencode").exists(), "OpenCode was not installed, so nothing is created for it");
let out = run(&h, &["--client", "claude-desktop"]);
assert!(String::from_utf8_lossy(&out.stdout).contains("not found"));
}
#[test]
fn a_file_that_is_not_json_is_left_alone() {
let h = home("bad");
std::fs::create_dir_all(h.join(".cursor")).unwrap();
std::fs::write(h.join(".cursor/mcp.json"), "{ // comment").unwrap();
let out = run(&h, &[]);
assert!(!out.status.success());
assert_eq!(std::fs::read_to_string(h.join(".cursor/mcp.json")).unwrap(), "{ // comment");
}
#[test]
fn claude_desktop_msix_folder_is_found() {
let h = home("msix");
let pkg = h.join("AppData/Local/Packages/Claude_abc/LocalCache/Roaming/Claude");
std::fs::create_dir_all(&pkg).unwrap();
// The MSIX folder is only read through LOCALAPPDATA, set for every platform here.
assert!(run(&h, &["--client", "claude-desktop"]).status.success());
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
}
}
+2
View File
@@ -22,6 +22,7 @@ solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
url = "2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
@@ -34,5 +35,6 @@ web-sys = { version = "0.3", features = [
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
] }
+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");
+64 -2
View File
@@ -500,8 +500,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 +528,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) {
+136
View File
@@ -0,0 +1,136 @@
//! Bounded HTTPS downloads, handed to the normal design-open command as unsaved copies.
use std::{cell::RefCell, rc::Rc};
use js_sys::{Reflect, Uint8Array};
use solidcraft_engine::io::vfs;
use solidcraft_ui_egui::{SolveApp, ui_kit};
use wasm_bindgen::{JsCast as _, JsValue};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use crate::{check_download_size, design_url};
type Reply = Result<(String, Vec<u8>), String>;
#[derive(Default)]
pub struct Download {
reply: Rc<RefCell<Option<Reply>>>,
pending: Option<(String, Vec<u8>)>,
}
impl Download {
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
let download = Self::default();
if let Some(url) = url {
let url = url.to_owned();
let slot = download.reply.clone();
let ctx = ctx.clone();
spawn_local(async move {
*slot.borrow_mut() = Some(fetch(&url).await);
ctx.request_repaint();
});
}
download
}
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
if let Some(reply) = self.reply.borrow_mut().take() {
match reply {
Ok(file) => self.pending = Some(file),
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
}
}
if self.pending.is_none() || app.file_picker_busy() {
return;
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
ui.horizontal(|ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
answer = Some(true);
}
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
answer = Some(false);
}
});
});
match answer {
Some(true) => {}
Some(false) => {
self.pending = None;
return;
}
None if closed => {
self.pending = None;
return;
}
None => return,
}
}
let Some((name, bytes)) = self.pending.take() else { return };
// The extension selects the design loader even when the URL has no extension.
let path = "/linked/design.solidcraft";
vfs::insert(path, bytes);
app.open_path(path);
if app.session.path.as_deref() == Some(path) {
app.session.path = None;
app.session.doc_mut().name = name;
app.session.mark_unsaved();
} else {
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
}
let _ = vfs::remove(path);
}
}
async fn fetch(value: &str) -> Reply {
let url = design_url(value)?;
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
let window = web_sys::window().ok_or("No browser window")?;
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
design_url(&response.url()).map_err(|_| "Design redirected to a URL that does not use HTTPS")?;
if !response.ok() {
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
}
let length = response
.headers()
.get("Content-Length")
.map_err(|_| "Cannot read download size")?
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
.transpose()?;
if let Some(length) = length {
check_download_size(0, length)?;
}
let stream = response.body().ok_or("Download has no body")?;
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
let result = read(&reader).await;
if result.is_err() {
let _ = JsFuture::from(reader.cancel()).await;
}
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
// can describe compressed bytes, so the streamed limit also checks the decoded size.
let bytes = result?;
Ok((name, bytes))
}
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
let mut bytes = Vec::new();
// Bound even a stream which keeps returning empty chunks.
for _ in 0..1_000_000 {
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
return Ok(bytes);
}
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
check_download_size(bytes.len(), u64::from(chunk.length()))?;
bytes.extend_from_slice(&chunk.to_vec());
}
Err("Download returned too many chunks".into())
}
+184 -23
View File
@@ -14,6 +14,8 @@ mod disk;
mod files;
#[cfg(target_arch = "wasm32")]
mod install;
#[cfg(target_arch = "wasm32")]
mod linked;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
@@ -25,6 +27,61 @@ fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
ctrl && key == "Tab"
}
#[cfg(any(target_arch = "wasm32", test))]
fn query_flag(query: &str, name: &str) -> bool {
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
}
/// Launch options use exact query keys; URL values are decoded only once.
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Default, Debug)]
struct Launch {
open: Option<String>,
embed: bool,
}
#[cfg(any(target_arch = "wasm32", test))]
impl Launch {
fn parse(query: &str) -> Self {
let mut launch = Self::default();
let mut embed = false;
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
match key.as_ref() {
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
"embed" => embed = true,
_ => {}
}
}
launch.embed = embed && launch.open.is_some();
launch
}
fn persists(&self) -> bool {
!self.embed
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn design_url(value: &str) -> Result<url::Url, String> {
if value.len() > 16_384 {
return Err("Design URL is too long".into());
}
let url = url::Url::parse(value).map_err(|_| "Invalid design URL")?;
if url.scheme() != "https" || url.host_str().is_none() || !url.username().is_empty() || url.password().is_some() {
return Err("Design links must use HTTPS without credentials".into());
}
Ok(url)
}
#[cfg(any(target_arch = "wasm32", test))]
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::rc::Rc;
@@ -112,6 +169,9 @@ mod web {
struct Shell {
app: SolveApp,
launch: crate::Launch,
linked: crate::linked::Download,
full_url: Option<String>,
api: Api,
files: Rc<Files>,
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
@@ -137,6 +197,9 @@ mod web {
impl Shell {
/// Files from the computer, writes to store, messages and the autosave.
fn files_tick(&mut self, ctx: &egui::Context) {
if !self.launch.persists() {
return;
}
if self.files.ctx.borrow().is_none() {
*self.files.ctx.borrow_mut() = Some(ctx.clone());
}
@@ -230,13 +293,15 @@ mod web {
fn sync_recents(&mut self) {
let mut entries = self.files.entries.borrow().clone();
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
let forgotten = self.app.home.forgotten.clone();
let recent = &mut self.app.home.recent;
let known = self.files.disk_known.borrow().clone();
// A design on the computer's disk stays listed while its file is remembered.
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
let p = format!("{}{}", files::OPFS, e.name);
if !recent.contains(&p) {
// A design the user removed from the list is not added back while it is stored.
if !recent.contains(&p) && !forgotten.contains(&p) {
recent.push(p);
}
}
@@ -245,6 +310,10 @@ mod web {
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
disk::prune(keep);
// Drop forgotten entries whose stored design is gone, so the set does not grow without bound.
let stored: std::collections::BTreeSet<String> =
entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)).map(|e| format!("{}{}", files::OPFS, e.name)).collect();
self.app.home.forgotten.retain(|p| stored.contains(p));
}
/// The stored designs: open, insert, rename, delete, download; open from the computer.
@@ -261,7 +330,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);
@@ -314,7 +383,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()));
@@ -383,6 +452,8 @@ mod web {
"delete" => {
files::delete(&self.files, &name);
self.app.home.recent.retain(|r| *r != path);
// The file is gone, so there is nothing left to keep forgotten.
self.app.home.forgotten.remove(&path);
}
"rename_cancel" => self.renaming = None,
"rename" => {
@@ -396,6 +467,10 @@ mod web {
*r = new.clone();
}
}
// A forgotten design follows its new name.
if self.app.home.forgotten.remove(&old) {
self.app.home.forgotten.insert(new);
}
}
Err(e) => self.app.set_status(e, true),
}
@@ -514,19 +589,42 @@ mod web {
if let Some(delay) = self.wheel_gestures.update(raw, now) {
ctx.request_repaint_after(delay);
}
files::take_drops(&self.files, raw);
if self.launch.persists() {
files::take_drops(&self.files, raw);
} else {
raw.dropped_files.clear();
}
// Before the app's own hook, which adds the control channel's events.
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
self.app.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.linked.tick(&mut self.app, ui.ctx());
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
if self.launch.embed {
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
{
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
}
});
}
self.app.ui(ui);
let ctx = ui.ctx().clone();
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
if self.launch.persists() {
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
}
self.watch_text_for_font(&ctx);
}
fn persist_egui_memory(&self) -> bool {
self.launch.persists()
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.launch.persists() {
return;
}
// Persist the user's own language, not a session-only English fallback.
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
@@ -545,15 +643,15 @@ mod web {
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
fn webgl() -> bool {
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
let q = query();
if q.contains("webgl") {
if crate::query_flag(&q, "webgl") {
if let Some(s) = &storage {
let _ = s.set_item(WEBGL_KEY, "1");
}
return true;
}
if q.contains("webgpu") {
if crate::query_flag(&q, "webgpu") {
if let Some(s) = &storage {
let _ = s.remove_item(WEBGL_KEY);
}
@@ -579,7 +677,10 @@ mod web {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
ask_before_unload(&window);
let launch = crate::Launch::parse(&query());
if launch.persists() {
ask_before_unload(&window);
}
leave_tab_switching_to_browser(&window);
let Some(document) = window.document() else { return };
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
@@ -599,19 +700,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.
@@ -636,26 +742,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);
@@ -664,8 +771,20 @@ mod web {
}
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
let font_language = app.preferences.language.clone();
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|mut url| {
url.set_query(None);
url.set_fragment(None);
if let Some(open) = &launch.open {
url.query_pairs_mut().append_pair("open", open);
}
url.to_string()
});
Ok(Box::new(Shell {
app,
launch,
linked,
full_url,
api,
files,
autosaved_at: f64::NEG_INFINITY,
@@ -709,6 +828,48 @@ fn main() {
mod tests {
use super::*;
#[test]
fn link_query_decodes_once_and_embed_disables_persistence() {
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
assert!(query_flag("?embed&webgl", "webgl"));
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
assert!(launch.embed);
assert!(!launch.persists());
assert!(!Launch::parse("?embed").embed);
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
assert!(Launch::parse("?open=https://example.org/a").persists());
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
}
#[test]
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
assert!(design_url("https://example.org/arm.solidcraft?download=1").is_ok());
for value in [
"http://example.org/a",
"data:application/json,{}",
"blob:https://example.org/a",
"javascript:alert(1)",
"/arm.solidcraft",
"https://user:secret@example.org/a",
"",
] {
assert!(design_url(value).is_err(), "{value}");
}
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384))).is_err());
assert!(design_url("http://redirect.example.org/a").is_err());
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
assert!(check_download_size(0, cap as u64).is_ok());
assert!(check_download_size(0, cap as u64 + 1).is_err());
assert!(check_download_size(cap - 2, 2).is_ok());
assert!(check_download_size(cap - 2, 3).is_err());
assert!(check_download_size(0, u64::MAX).is_err());
assert!(check_download_size(cap, 1).is_err());
assert!(check_download_size(usize::MAX, 0).is_err());
}
#[test]
fn only_ctrl_tab_is_left_to_the_browser() {
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
+6 -4
View File
@@ -14,7 +14,7 @@ raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
# (Windows resize border) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
# (Windows resize border, taskbar icon) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
[lints.rust]
unsafe_code = "deny"
@@ -54,7 +54,8 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
# The main window's handle for src/native_frame.rs (already in the tree through eframe and winit).
raw-window-handle = "0.6"
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
# The window's resize border, DPI size and class icon (src/native_frame.rs): the window procedure, hit
# test, DPI, the exe's icon.
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
windows-sys = { version = "0.61", features = [
"Win32_Devices_DeviceAndDriverInstallation",
@@ -81,9 +82,10 @@ solidcraft-3dx-client = { workspace = true }
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
# driver does not shut the app out.
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
# inset (src/main.rs); all already come with eframe and rfd.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
+40 -4
View File
@@ -127,7 +127,7 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
OPEN.fetch_sub(1, Ordering::Relaxed);
let mut refused = stream;
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
continue;
}
let tx = tx.clone();
@@ -174,6 +174,15 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
}
}
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
/// of its pieces apart, a system call for every string, number and comma: half a second for a
/// design's component tree when it has thousands of components.
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
line.push(b'\n');
out.write_all(&line)
}
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
@@ -184,7 +193,7 @@ fn unauthorized() -> Value {
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
use std::io::Read;
let _ = writeln!(out, "{}", unauthorized());
let _ = send(out, &unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
@@ -381,7 +390,7 @@ fn serve_with(
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
Ok(None) => break,
Err(e) => {
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
let _ = send(&mut out, &json!({"ok": false, "error": e.to_string()}));
break;
}
};
@@ -463,7 +472,7 @@ fn serve_with(
json!({"ok": false, "error": format!("bad JSON: {e}")})
}
};
if writeln!(out, "{reply}").is_err() {
if send(&mut out, &reply).is_err() {
break;
}
if bad >= MAX_BAD_LINES {
@@ -750,4 +759,31 @@ mod tests {
let mut r = BufReader::with_capacity(64, text.as_slice());
assert!(read_line(&mut r, 1000).is_err());
}
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
/// thousands of components took half a second to send), and reads back as it was.
#[test]
fn a_large_reply_is_written_at_once() {
struct Counted {
writes: usize,
bytes: Vec<u8>,
}
impl Write for Counted {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writes += 1;
self.bytes.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
let mut out = Counted { writes: 0, bytes: Vec::new() };
send(&mut out, &reply).unwrap();
assert_eq!(out.writes, 1);
assert_eq!(out.bytes.last(), Some(&b'\n'));
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
}
}
+224 -25
View File
@@ -111,6 +111,13 @@ impl eframe::App for App {
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
}
solidcraft_engine::crash::phase(solidcraft_engine::crash::RUNNING);
// A DMG install has no installer step: put solidcraft-cli on PATH, quietly.
#[cfg(target_os = "macos")]
std::thread::spawn(solidcraft_ui_egui::cli_link::install_on_launch);
// Automation runs (a control port was asked for) leave the user's AI apps alone.
if !self.take_focus_unwanted {
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
}
if let Some(dir) = self.starting.take() {
startup::started(&dir);
}
@@ -147,6 +154,8 @@ impl eframe::App for App {
if let Some(menu) = &mut self.native_menu {
menu.raw_input_hook(&mut self.solve, ctx, raw);
}
#[cfg(target_os = "macos")]
macos_fullscreen_content(ctx, raw);
// Before the app's own hook, which adds the control channel's events.
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
@@ -156,7 +165,7 @@ impl eframe::App for App {
self.solve.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.solve.ui(ui);
content_ui(&mut self.solve, ui);
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
@@ -166,7 +175,13 @@ impl eframe::App for App {
solidcraft_ui_egui::agent_chat::shutdown(&mut self.solve);
// A save still being written ends before the app does.
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
// Pictures still being drawn off the window thread (Home cards, file previews) end
// before the GPU is torn down: one drawn while the process exits can crash the driver.
if !solidcraft_engine::capture::close(std::time::Duration::from_secs(5)) {
log::warn!("a picture was still being drawn as the app closed");
}
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
solidcraft_engine::crash::end_session();
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
@@ -232,6 +247,37 @@ fn case_variants(exts: &[&str]) -> Vec<String> {
all
}
/// egui's root Ui spans the whole viewport, including native safe areas. Lay out the app
/// inside the unobscured rectangle so its application bar can receive native pointer input.
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
let content = ui.ctx().content_rect();
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
}
/// A transparent macOS title bar still intercepts clicks at the top in fullscreen (#3643).
/// `contentLayoutRect` reports the whole content view once that bar auto-hides, so reserve
/// the native titled-window height even then. AppKit supplies the height in logical points;
/// egui's UI zoom is separate from the display's Retina scale.
#[cfg(target_os = "macos")]
fn macos_fullscreen_content(ctx: &egui::Context, raw: &mut egui::RawInput) {
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
let frame = objc2_app_kit::NSWindow::frameRectForContentRect_styleMask(Default::default(), objc2_app_kit::NSWindowStyleMask::Titled, mtm);
fullscreen_content_insets(raw, frame.size.height as f32, ctx.zoom_factor());
}
#[cfg(any(target_os = "macos", test))]
fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom: f32) {
let fullscreen = raw.viewports.get(&raw.viewport_id).and_then(|v| v.fullscreen).unwrap_or(false);
let mut insets = raw.safe_area_insets.unwrap_or_default();
if fullscreen && title_height.is_finite() && title_height > 0.0 && zoom.is_finite() && zoom > 0.0 {
insets.0.top = insets.0.top.max(title_height / zoom);
}
// Explicitly reset on leaving fullscreen: None would retain the preceding frame's inset.
raw.safe_area_insets = Some(insets);
}
fn services(cc: &eframe::CreationContext<'_>) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
@@ -246,7 +292,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));
}
@@ -289,6 +337,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)),
@@ -369,7 +418,7 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
let agent: ureq::Agent = ureq::Agent::config_builder()
.tls_config(tls)
.http_status_as_error(false)
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 900))))
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 3600))))
.timeout_connect(Some(Duration::from_secs(10)))
.user_agent("SolidCraft")
.max_redirects(0)
@@ -529,13 +578,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
@@ -551,7 +609,8 @@ const FLOAT_DEPTH_BITS: u8 = 32;
const STENCIL_BITS: u8 = 8;
const MSAA: u16 = 4;
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
// Let macOS use the bundle's layered icon; eframe's default bitmap would replace it.
#[cfg(target_os = "macos")]
let icon = egui::IconData::default();
@@ -564,7 +623,7 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
.with_inner_size([1600.0, 1000.0])
.with_min_inner_size([960.0, 600.0])
.with_drag_and_drop(true)
.with_decorations(!CUSTOM_TITLEBAR)
.with_decorations(!custom_titlebar)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false)
@@ -638,7 +697,7 @@ 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 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).
@@ -656,10 +715,16 @@ fn main() -> eframe::Result {
// module on Windows, and on every system the startup step the app was in.
#[cfg(target_os = "windows")]
native_crash::install();
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
let started = match &wgpu_options.wgpu_setup {
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
_ => eframe::wgpu::Backends::all(),
};
let custom_titlebar = CUSTOM_TITLEBAR && !startup::system_title_bar(cfg!(target_os = "windows"), started);
eframe::egui_wgpu::set_startup_hook(startup_step);
solidcraft_engine::crash::phase("window");
let options = eframe::NativeOptions {
viewport: app_viewport()?,
viewport: app_viewport(custom_titlebar)?,
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
depth_buffer: 0,
@@ -723,13 +788,18 @@ fn main() -> eframe::Result {
mark("fonts");
let mut app = SolveApp::new(Session::default(), services(cc));
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
app.custom_titlebar = CUSTOM_TITLEBAR;
// Windows: the OS hit test offers the resize border, at the window's DPI, from the
// first message (#1381).
app.custom_titlebar = custom_titlebar;
#[cfg(target_os = "windows")]
{
app.native_resize_border = CUSTOM_TITLEBAR && native_frame::install(cc);
if CUSTOM_TITLEBAR && !app.native_resize_border {
// Windows: the taskbar button shows the mark even when slow first frames keep the
// taskbar waiting for the window's icon.
if !native_frame::set_class_icon(cc) {
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
}
// The OS hit test offers the resize border, at the window's DPI, from the first
// message (#1381).
app.native_resize_border = custom_titlebar && native_frame::install(cc);
if custom_titlebar && !app.native_resize_border {
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
}
}
@@ -744,19 +814,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
@@ -781,6 +869,9 @@ fn main() -> eframe::Result {
app.session.mark_saved();
app.session.undo.clear();
}
// Lock files next to open designs: a design open on another computer says so (#5467),
// the files given at launch included.
solidcraft_ui_egui::open_elsewhere::start(&mut app);
// A launch from Finder: its files arrived before the window.
#[cfg(target_os = "macos")]
let files = {
@@ -871,9 +962,12 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
eprintln!("SolidCraft graphics: starting {backends:?}");
setup.instance_descriptor.backends = backends;
}
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
}
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());
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| {
@@ -991,11 +1085,11 @@ mod backend_tests {
}
#[cfg(test)]
mod icon_tests {
mod viewport_tests {
#[cfg(target_os = "macos")]
#[test]
fn macos_preserves_the_bundle_icon_instead_of_setting_a_runtime_bitmap() {
let viewport = super::app_viewport().expect("viewport");
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
assert_eq!(viewport.icon.as_deref(), Some(&eframe::egui::IconData::default()));
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
}
@@ -1003,12 +1097,89 @@ mod icon_tests {
#[cfg(not(target_os = "macos"))]
#[test]
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
let viewport = super::app_viewport().expect("viewport");
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
let icon = viewport.icon.expect("running window must have an icon");
assert_eq!((icon.width, icon.height), (64, 64));
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
}
#[test]
fn fullscreen_bar_clears_native_chrome_and_undo_redo_keep_working() {
use egui::{Event, PointerButton, Pos2, RawInput, Rect, ViewportId, vec2};
use serde_json::json;
use solidcraft_ui_egui::scenario::handle_at;
// AppKit points are independent of Retina scale and of the user's UI zoom.
for (zoom, retina) in [(1.0, 1.0), (1.0, 2.0), (1.5, 2.0), (2.0, 2.0)] {
let ctx = egui::Context::default();
solidcraft_ui_egui::fonts::setup_bundled_fonts(&ctx);
ctx.set_zoom_factor(zoom);
let mut app = super::SolveApp::new(super::Session::default(), Default::default());
app.integrated_titlebar = true;
for (command, params) in [
("sketch.create", json!({"plane":"XY"})),
("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})),
("sketch.finish", json!({})),
("solid.extrude", json!({"distance":15})),
] {
app.run(command, params).unwrap();
}
let mut time = 0.0;
let mut frame = |app: &mut super::SolveApp, fullscreen, events| {
time += 0.05;
let mut raw = RawInput {
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
time: Some(time),
events,
..Default::default()
};
let viewport = raw.viewports.get_mut(&ViewportId::ROOT).unwrap();
viewport.fullscreen = Some(fullscreen);
viewport.native_pixels_per_point = Some(retina);
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
};
// Enter, leave, and enter again: no stale inset when returning to a normal window.
for fullscreen in [false, true, false, true] {
frame(&mut app, fullscreen, vec![]);
frame(&mut app, fullscreen, vec![]);
let top = if fullscreen { 32.0 / zoom } else { 0.0 };
for name in ["appbar:file", "appbar:undo", "appbar:redo", "appbar:help"] {
let at = handle_at(name).unwrap();
assert!((at.y - top - 17.0).abs() < 0.1, "{name}: {at:?}, fullscreen={fullscreen}, zoom={zoom}");
}
for (handle, bodies) in [("appbar:undo", 0), ("appbar:redo", 1)] {
let at = handle_at(handle).unwrap();
frame(&mut app, fullscreen, vec![Event::PointerMoved(at)]);
for pressed in [true, false] {
frame(
&mut app,
fullscreen,
vec![Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }],
);
}
assert_eq!(app.session.model.state().bodies.len(), bodies, "{handle}, fullscreen={fullscreen}, zoom={zoom}");
}
}
}
}
#[test]
fn fullscreen_inset_preserves_existing_safe_areas_and_resets_on_exit() {
use egui::epaint::MarginF32;
use egui::{RawInput, SafeAreaInsets, ViewportId};
let mut raw = RawInput::default();
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(true);
raw.safe_area_insets = Some(SafeAreaInsets(MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 }));
super::fullscreen_content_insets(&mut raw, 32.0, 2.0);
assert_eq!(raw.safe_area_insets.unwrap().0, MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 });
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(false);
raw.safe_area_insets = None;
super::fullscreen_content_insets(&mut raw, 32.0, 1.0);
assert_eq!(raw.safe_area_insets, Some(SafeAreaInsets::default()));
}
}
#[cfg(test)]
@@ -1046,6 +1217,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;
+57 -5
View File
@@ -15,10 +15,14 @@
//! window rescales once at the crossing and stays under the pointer, instead of the OS's move
//! loop and winit disagreeing about a frame the window doesn't have.
//!
//! Replacing a window procedure has no safe API, hence the `unsafe_code` allowance on this
//! module (the app crate denies it; see `native_crash.rs` and `open_documents.rs` for the others).
//! Replacing a window procedure and setting a window class's icon have no safe API, hence the
//! `unsafe_code` allowance on this module (the app crate denies it; see `native_crash.rs` and
//! `open_documents.rs` for the others).
//! The procedure is installed once, on the thread that owns the window, for the main window only,
//! and calls the previous one (winit's) for everything it doesn't answer.
//!
//! [`set_class_icon`] gives the window's class the exe's icon, so the taskbar button shows the
//! mark even when the window is too busy to answer the taskbar's request for its icon.
#![allow(unsafe_code)]
use std::sync::atomic::{AtomicIsize, Ordering};
@@ -27,12 +31,18 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use solidcraft_ui_egui::titlebar;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallWindowProcW, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT,
HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsIconic, IsZoomed, SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SetWindowLongPtrW, WM_GETDPISCALEDSIZE,
WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
CallWindowProcW, GCLP_HICON, GCLP_HICONSM, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT,
HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IMAGE_ICON, IsIconic, IsZoomed, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON,
SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SM_CXSMICON, SetWindowLongPtrW, WM_GETDPISCALEDSIZE, WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
};
// windows-sys has SetClassLongPtrW only on 64-bit targets; on 32-bit Windows it is SetClassLongW.
#[cfg(target_pointer_width = "64")]
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
#[cfg(target_pointer_width = "32")]
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongW as SetClassLongPtrW;
/// The work area (the monitor without the taskbar) of the monitor nearest the window, in physical
/// pixels: left, top, width, height.
@@ -54,6 +64,48 @@ pub fn work_area(window: &impl HasWindowHandle) -> Option<(i32, i32, i32, i32)>
Some((rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top))
}
/// The exe's icon resource (`build.rs` embeds it as `1 ICON`).
const ICON_RESOURCE: usize = 1;
/// Give the main window's class the exe's icon, big and small. Call it before the window is
/// shown (eframe's app creator).
///
/// The taskbar asks a window for its icon when the window appears. eframe sets the window's
/// icon before that, while the window is still hidden, and then shows the window at the end of
/// its first frame; if that frame is slow, or startup kept the thread busy long enough for
/// Windows to mark the window as not responding, the request isn't answered in time. The taskbar then
/// takes the window class's icon and doesn't ask again. winit registers its class without one,
/// so the button showed Windows' generic icon for as long as the window was open.
/// `false` if it isn't a Win32 window or the exe's icon doesn't load; SetClassLongPtrW's own
/// failure can't be told from a class that had no icon (both return 0), so it isn't reported.
pub fn set_class_icon(window: &impl HasWindowHandle) -> bool {
let Ok(handle) = window.window_handle() else { return false };
let RawWindowHandle::Win32(w) = handle.as_raw() else { return false };
let hwnd = w.hwnd.get() as HWND;
// SAFETY: a plain query on the live main window.
let dpi = unsafe { GetDpiForWindow(hwnd) };
let (Some(big), Some(small)) = (exe_icon(SM_CXICON, dpi), exe_icon(SM_CXSMICON, dpi)) else { return false };
// SAFETY: `hwnd` is the live main window and both icons are valid handles. The class keeps
// them for the rest of the process, so they are never destroyed. `as _`: on 32-bit Windows
// this is SetClassLongW (see the imports), which takes an i32.
unsafe {
SetClassLongPtrW(hwnd, GCLP_HICON, big as _);
SetClassLongPtrW(hwnd, GCLP_HICONSM, small as _);
}
true
}
/// The exe's icon at the size of system metric `metric` (`SM_CXICON`, `SM_CXSMICON`) at `dpi`.
fn exe_icon(metric: i32, dpi: u32) -> Option<windows_sys::Win32::Foundation::HANDLE> {
// SAFETY: GetModuleHandleW(null) is this exe, loaded for the life of the process; the name
// is an integer resource id (MAKEINTRESOURCE), which LoadImageW accepts in place of a string.
let icon = unsafe {
let size = GetSystemMetricsForDpi(metric, dpi);
LoadImageW(GetModuleHandleW(std::ptr::null()), std::ptr::without_provenance(ICON_RESOURCE), IMAGE_ICON, size, size, LR_DEFAULTCOLOR)
};
(!icon.is_null()).then_some(icon)
}
/// The window procedure ours replaced (winit's), as an address; 0 before [`install`].
static PREVIOUS: AtomicIsize = AtomicIsize::new(0);
+1
View File
@@ -105,6 +105,7 @@ impl NativeMenu {
("messages", "Messages\u{2026}"),
("report", "Report an Issue\u{2026}"),
("crashes", "Crash Reports\u{2026}"),
("install_cli", "Install solidcraft-cli command"),
] {
let item = MenuItem::new(app.tr_label(label), true, None);
help.append(&item)?;
+311 -28
View File
@@ -521,6 +521,119 @@ fn hid_api() -> hidapi::HidResult<HidApi> {
HidApi::new()
}
/// An open 3D-mouse interface: the device, how its reports are laid out, and its vendor and
/// product ids.
#[cfg(any(target_os = "windows", target_os = "macos", test))]
struct Candidate<D> {
device: D,
layout: Layout,
ids: (u16, u16),
}
/// Input reports read with a timeout in milliseconds: hidapi's devices, and the tests' simulated
/// ones. `Ok(0)` is a timeout; an error means the device is gone.
#[cfg(any(target_os = "windows", target_os = "macos", test))]
trait ReportSource {
fn read_report(&self, report: &mut [u8], timeout_ms: i32) -> Result<usize, String>;
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
impl ReportSource for hidapi::HidDevice {
fn read_report(&self, report: &mut [u8], timeout_ms: i32) -> Result<usize, String> {
self.read_timeout(report, timeout_ms).map_err(|error| error.to_string())
}
}
/// What following the candidates tells the reader.
#[cfg(any(target_os = "windows", target_os = "macos", test))]
enum Followed<'a> {
/// This device is the one read now, so its calibration applies. With the buttons it last
/// reported down, when it reported them while another was read: they are its baseline, so a
/// press that makes it the device in use counts as a press, not as a button already held.
Device((u16, u16), Option<u32>),
/// An input report from it.
Report(&'a Layout, &'a [u8]),
/// It is gone.
Gone,
}
/// Whether a report shows its device in use: an axis off zero or a button down. A cap at rest
/// sends zeros at most, and an idle 3Dconnexion Universal Receiver sends nothing at all.
#[cfg(any(target_os = "windows", target_os = "macos", test))]
fn shows_use(layout: Layout, report: &[u8]) -> bool {
layout.decode(report).is_some_and(|decoded| decoded.axes.iter().flatten().any(|axis| *axis != 0) || decoded.buttons.is_some_and(|down| down != 0))
}
/// Reads every candidate and follows the one in use (#3988). The first is followed from the start,
/// as when it was the only one read. Another takes over once it shows use while the followed one
/// has shown none for `STALE_AFTER`, so two devices moved together don't hand the reading back
/// and forth. An idle Universal Receiver opens fine and never fails; read on its own it kept a
/// SpaceMouse next to it from ever being read. A candidate that fails is dropped. If it was the
/// followed one, the reading is reset and the next takes over. Returns when none is left.
#[cfg(any(target_os = "windows", target_os = "macos", test))]
fn follow<D: ReportSource>(mut candidates: Vec<Candidate<D>>, now: impl Fn() -> Instant, mut emit: impl FnMut(Followed<'_>)) {
let Some(first) = candidates.first() else { return };
emit(Followed::Device(first.ids, None));
let mut followed = 0;
// When the followed device last showed use.
let mut used_at: Option<Instant> = None;
// The buttons each candidate last reported down, by index, as long as it has reported them.
let mut buttons: Vec<Option<u32>> = candidates.iter().map(|_| None).collect();
let mut report = [0_u8; REPORT_BUFFER_BYTES];
while !candidates.is_empty() {
// One device is waited on as before; several take turns, briefly, so none waits behind
// an idle one. Reports queue in the system meanwhile, so none is lost.
let timeout_ms = if candidates.len() == 1 { 100 } else { 10 };
let mut index = 0;
while let Some(candidate) = candidates.get(index) {
match candidate.device.read_report(&mut report, timeout_ms) {
Ok(0) => {}
Ok(size) => {
let bytes = report.get(..size).unwrap_or_default();
let before = buttons.get(index).copied().flatten();
if let (Some(seen), Some(down)) = (buttons.get_mut(index), candidate.layout.decode(bytes).and_then(|decoded| decoded.buttons)) {
*seen = Some(down);
}
if shows_use(candidate.layout, bytes) {
let at = now();
if index != followed && used_at.is_none_or(|used| at.duration_since(used) >= STALE_AFTER) {
let (vendor, product) = candidate.ids;
log::info!("3D mouse: reading {vendor:04x}:{product:04x}, the device in use");
followed = index;
emit(Followed::Device(candidate.ids, before));
}
if index == followed {
used_at = Some(at);
}
}
if index == followed {
emit(Followed::Report(&candidate.layout, bytes));
}
}
Err(error) => {
log::info!("3D mouse disconnected: {error}");
candidates.remove(index);
if index < buttons.len() {
buttons.remove(index);
}
if index == followed {
emit(Followed::Gone);
followed = 0;
used_at = None;
if let Some(next) = candidates.first() {
emit(Followed::Device(next.ids, buttons.first().copied().flatten()));
}
} else if index < followed {
followed -= 1;
}
continue;
}
}
index += 1;
}
}
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
let Ok(mut api) = hid_api() else {
@@ -528,36 +641,38 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
return;
};
loop {
if let Some((device, layout, ids)) = candidates(&mut api).into_iter().find_map(|path| {
let device = api
.open_path(&path)
.inspect_err(|error| {
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
// The driver having the device is the reason it cannot be opened, not a problem.
if !DRIVER_CONNECTED.load(Ordering::Relaxed) && !DRIVER_REGISTERED.load(Ordering::Relaxed) {
log::warn!("3D mouse: cannot open the device: {error}");
}
})
.ok()?;
// Read when the device was opened, from this device alone.
let info = device.get_device_info().ok()?;
let layout = device_layout(&device, info.vendor_id())?;
Some((device, layout, (info.vendor_id(), info.product_id())))
}) {
connected(&ctx, &shared, Source::Hid(Some(ids)));
let mut report = [0_u8; REPORT_BUFFER_BYTES];
loop {
match device.read_timeout(&mut report, 100) {
Ok(0) => {}
Ok(size) => publish(&ctx, &shared, layout, report.get(..size).unwrap_or_default()),
Err(error) => {
log::info!("3D mouse disconnected: {error}");
break;
}
let open: Vec<_> = candidates(&mut api)
.into_iter()
.filter_map(|path| {
let device = api
.open_path(&path)
.inspect_err(|error| {
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
// The driver having the device is the reason it cannot be opened, not a problem.
if !DRIVER_CONNECTED.load(Ordering::Relaxed) && !DRIVER_REGISTERED.load(Ordering::Relaxed) {
log::warn!("3D mouse: cannot open the device: {error}");
}
})
.ok()?;
// Read when the device was opened, from this device alone.
let info = device.get_device_info().ok()?;
let layout = device_layout(&device, info.vendor_id())?;
Some(Candidate { device, layout, ids: (info.vendor_id(), info.product_id()) })
})
.collect();
follow(open, Instant::now, |event| match event {
Followed::Device(ids, buttons) => {
connected(&ctx, &shared, Source::Hid(Some(ids)));
// Taken over from another device: what it reported meanwhile is its baseline.
if let Some(buttons) = buttons
&& let Ok(mut reading) = shared.lock()
{
reading.set_buttons(buttons);
}
}
reset(&ctx, &shared);
}
Followed::Report(layout, report) => publish(&ctx, &shared, *layout, report),
Followed::Gone => reset(&ctx, &shared),
});
std::thread::sleep(RETRY_AFTER);
}
}
@@ -1294,6 +1409,174 @@ mod tests {
assert!(shared.lock().unwrap().arrived, "but the report did arrive");
}
/// A simulated 3D-mouse interface. Each read takes the next scripted step, a report or a
/// timeout (empty), and sets the shared clock to the step's time. Once the script runs out,
/// the device is unplugged.
struct Simulated<'a> {
clock: &'a std::cell::Cell<Instant>,
start: Instant,
steps: std::cell::RefCell<std::collections::VecDeque<(u64, Vec<u8>)>>,
}
impl ReportSource for Simulated<'_> {
fn read_report(&self, report: &mut [u8], _timeout_ms: i32) -> Result<usize, String> {
let (ms, bytes) = self.steps.borrow_mut().pop_front().ok_or_else(|| "unplugged".to_string())?;
self.clock.set(self.start + Duration::from_millis(ms));
report.get_mut(..bytes.len()).ok_or_else(|| "report too long".to_string())?.copy_from_slice(&bytes);
Ok(bytes.len())
}
}
#[derive(Debug, PartialEq)]
enum Seen {
/// The product id now read.
Device(u16),
/// The buttons it had reported down before it was read, its baseline.
Baseline(u32),
/// A report published, the product id read when it was, and whether it showed use.
Report(u16, Vec<u8>, bool),
Gone,
}
/// Follows simulated devices, given as (product id, scripted steps in ms), and returns what
/// the reader was told.
fn follow_simulated(devices: &[(u16, &[(u64, &[u8])])]) -> Vec<Seen> {
follow_simulated_with(Layout::default(), devices)
}
/// As `follow_simulated`, with every device's reports laid out as `layout`.
fn follow_simulated_with(layout: Layout, devices: &[(u16, &[(u64, &[u8])])]) -> Vec<Seen> {
let start = Instant::now();
let clock = std::cell::Cell::new(start);
let candidates = devices
.iter()
.map(|(product, steps)| Candidate {
device: Simulated {
clock: &clock,
start,
steps: std::cell::RefCell::new(steps.iter().map(|(ms, bytes)| (*ms, bytes.to_vec())).collect()),
},
layout,
ids: (0x256f, *product),
})
.collect();
let (mut seen, mut read) = (Vec::new(), 0);
follow(
candidates,
|| clock.get(),
|event| match event {
Followed::Device((_, product), buttons) => {
read = product;
seen.push(Seen::Device(product));
seen.extend(buttons.map(Seen::Baseline));
}
Followed::Report(layout, bytes) => seen.push(Seen::Report(read, bytes.to_vec(), shows_use(*layout, bytes))),
Followed::Gone => seen.push(Seen::Gone),
},
);
seen
}
const RECEIVER: u16 = 0xc652;
const SPACEMOUSE: u16 = 0xc63a;
const PUSH_X: &[u8] = &[1, 100, 0, 0, 0, 0, 0];
const AT_REST: &[u8] = &[1, 0, 0, 0, 0, 0, 0];
const TILT: &[u8] = &[2, 0, 0, 50, 0, 0, 0];
/// #3988: a Universal Receiver with nothing paired is found first and never fails, and a wired
/// SpaceMouse next to it was never read. The receiver is followed only until the SpaceMouse
/// moves; the SpaceMouse's reports, its zeros included, are read from then on.
#[test]
fn an_idle_receiver_does_not_keep_a_spacemouse_from_being_read() {
let receiver: &[(u64, &[u8])] = &[(0, &[]), (10, &[]), (20, &[]), (30, &[])];
let spacemouse: &[(u64, &[u8])] = &[(5, &[]), (15, PUSH_X), (25, AT_REST), (35, TILT)];
assert_eq!(
follow_simulated(&[(RECEIVER, receiver), (SPACEMOUSE, spacemouse)]),
[
Seen::Device(RECEIVER),
Seen::Device(SPACEMOUSE),
Seen::Report(SPACEMOUSE, PUSH_X.to_vec(), true),
Seen::Report(SPACEMOUSE, AT_REST.to_vec(), false),
Seen::Report(SPACEMOUSE, TILT.to_vec(), true),
// The receiver is unplugged first, then the SpaceMouse, the one being read.
Seen::Gone,
]
);
}
/// Two devices in use at once don't trade the reading: the other takes over only after the
/// followed one has shown no use for `STALE_AFTER`. A report at rest or with an id the layout
/// doesn't know shows no use, and only the followed device's reports are published.
#[test]
fn another_device_takes_over_only_once_the_followed_one_is_still() {
let unknown_id: &[u8] = &[0x17, 80];
let first: &[(u64, &[u8])] = &[(0, PUSH_X), (100, AT_REST), (200, &[])];
let second: &[(u64, &[u8])] = &[(50, PUSH_X), (150, unknown_id), (250, TILT)];
assert_eq!(
follow_simulated(&[(0xc635, first), (SPACEMOUSE, second)]),
[
Seen::Device(0xc635),
Seen::Report(0xc635, PUSH_X.to_vec(), true),
// The second moves 50 ms after the first did: not yet. The first's own rest
// report is read, and the second's unknown report is not.
Seen::Report(0xc635, AT_REST.to_vec(), false),
// 250 ms after the first last moved, the second takes over.
Seen::Device(SPACEMOUSE),
Seen::Report(SPACEMOUSE, TILT.to_vec(), true),
Seen::Gone,
]
);
}
/// One device is read as before: every report, until it fails. When the followed one of
/// several fails, the reading is reset and the next one is read.
#[test]
fn a_device_that_fails_hands_the_reading_to_the_next() {
let only: &[(u64, &[u8])] = &[(0, &[]), (1, AT_REST), (2, &[0x17, 80])];
assert_eq!(
follow_simulated(&[(SPACEMOUSE, only)]),
[
Seen::Device(SPACEMOUSE),
Seen::Report(SPACEMOUSE, AT_REST.to_vec(), false),
Seen::Report(SPACEMOUSE, vec![0x17, 80], false),
Seen::Gone
]
);
let unplugged: &[(u64, &[u8])] = &[];
let next: &[(u64, &[u8])] = &[(1, PUSH_X)];
assert_eq!(
follow_simulated(&[(SPACEMOUSE, unplugged), (RECEIVER, next)]),
[Seen::Device(SPACEMOUSE), Seen::Gone, Seen::Device(RECEIVER), Seen::Report(RECEIVER, PUSH_X.to_vec(), true), Seen::Gone]
);
assert_eq!(follow_simulated(&[]), []);
}
/// A button press that makes a device the one in use is a press. The buttons it reported while
/// another was read are its baseline; the reader keeps them, so the press isn't taken for a
/// button already held, as a device's first button report is.
#[test]
fn a_press_that_takes_over_counts_as_a_press() {
let mut layout = Layout::default();
layout.buttons[0] =
Some(solidcraft_space_mouse::Field { report_id: 3, bit_offset: 0, bits: 1, logical_min: 0, logical_max: 1, relative: false });
let (none_down, first_down): (&[u8], &[u8]) = (&[3, 0], &[3, 1]);
let receiver: &[(u64, &[u8])] = &[(0, &[]), (10, &[]), (20, &[])];
let spacemouse: &[(u64, &[u8])] = &[(5, none_down), (15, first_down), (25, &[])];
assert_eq!(
follow_simulated_with(layout, &[(RECEIVER, receiver), (SPACEMOUSE, spacemouse)]),
[Seen::Device(RECEIVER), Seen::Device(SPACEMOUSE), Seen::Baseline(0), Seen::Report(SPACEMOUSE, first_down.to_vec(), true), Seen::Gone,]
);
// What the reader does with that: the press after the baseline is a tap.
let mut reading = Reading { buttons_known: false, ..Reading::new(Instant::now()) };
reading.set_buttons(0);
assert!(decode_report(first_down, layout, &mut reading, Instant::now()));
assert_eq!((reading.buttons, reading.taps), (1, 1));
// Without one, the same report is the baseline: held, not pressed.
let mut unseen = Reading { buttons_known: false, ..Reading::new(Instant::now()) };
assert!(decode_report(first_down, layout, &mut unseen, Instant::now()));
assert_eq!((unseen.buttons, unseen.taps), (1, 0));
}
#[test]
fn a_held_reading_stays_until_the_next_one_and_a_hid_reading_goes_stale() {
let now = Instant::now();
+132
View File
@@ -129,6 +129,48 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
})
}
/// Adapter properties used to choose a card without opening a device.
#[derive(Clone, Debug)]
pub struct AdapterChoice {
pub name: String,
pub compatible: bool,
}
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
/// environment override follows wgpu's case-insensitive substring convention.
/// No match leaves selection to the normal wgpu request.
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
if let Some(name) = environment.filter(|s| !s.is_empty()) {
let name = name.to_lowercase();
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
return Some(index);
}
}
if let Some(name) = preference.filter(|s| !s.is_empty())
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
{
return Some(index);
}
None
}
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
if cpu {
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
let choices: Vec<_> = adapters
.iter()
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
.collect();
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
}));
}
}
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
@@ -208,6 +250,29 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
}
/// The environment variable that switches wgpu's indirect-call validation on or off.
pub const INDIRECT_VALIDATION_ENV: &str = "WGPU_VALIDATION_INDIRECT_CALL";
/// The instance flags for the window. wgpu's indirect-call validation compiles a compute shader
/// when the device is created, and on Intel Macs (macOS 12 with Intel Iris graphics, #3024,
/// #3080, #4605) the Metal compiler refuses it: the device is lost and the window never opens.
/// So on an Intel Mac the validation is left off, unless the user turned it on or off with
/// `WGPU_VALIDATION_INDIRECT_CALL` themselves.
pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool, env: Option<&str>) -> eframe::wgpu::InstanceFlags {
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
}
/// Windows with OpenGL: the window keeps the system title bar (#2063). The window without OS
/// decorations keeps its caption style while having no frame, and some OpenGL drivers (older
/// Intel HD/UHD) then show the picture a caption's height lower than the window, so it no longer
/// lines up with the pointer. Direct3D 12 and Vulkan draw it in place and keep the application bar
/// as the title bar. `backends` are the ones wgpu starts: OpenGL alone is decided before the
/// window opens.
pub fn system_title_bar(windows: bool, backends: eframe::wgpu::Backends) -> bool {
use eframe::wgpu::Backends;
windows && backends.contains(Backends::GL) && !backends.intersects(Backends::DX12 | Backends::VULKAN)
}
/// The message shown when no window could be opened at all.
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
@@ -226,6 +291,48 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
mod tests {
use super::*;
fn cards() -> Vec<AdapterChoice> {
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
}
#[test]
fn adapter_names_environment_override_and_missing_card_fallback() {
let cards = cards();
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
}
#[test]
fn automatic_adapter_leaves_selection_to_wgpu() {
let mut cards = cards();
assert_eq!(choose_adapter(&cards, None, None), None);
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
cards[2].compatible = false;
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
assert_eq!(choose_adapter(&[], None, None), None);
}
#[test]
fn automatic_adapter_preserves_the_previous_request_configuration() {
use eframe::egui_wgpu::WgpuSetupCreateNew;
let previous = WgpuSetupCreateNew::without_display_handle();
for preference in [None, Some(String::new())] {
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, None, preference);
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
assert_eq!(setup.power_preference, previous.power_preference);
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
}
let mut setup = WgpuSetupCreateNew::without_display_handle();
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
}
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
@@ -246,6 +353,19 @@ mod tests {
assert!(settings_directory(Some(file.as_os_str()), Some(default)).is_err());
}
#[test]
fn intel_macs_start_without_indirect_call_validation() {
use eframe::wgpu::InstanceFlags as F;
let on = F::VALIDATION | F::VALIDATION_INDIRECT_CALL;
assert_eq!(window_instance_flags(on, true, None), F::VALIDATION);
assert_eq!(window_instance_flags(on, true, Some(" ")), F::VALIDATION);
// The user's own choice stands, on or off.
assert_eq!(window_instance_flags(on, true, Some("1")), on);
assert_eq!(window_instance_flags(F::VALIDATION, true, Some("0")), F::VALIDATION);
// Every other machine keeps what it had.
assert_eq!(window_instance_flags(on, false, None), on);
}
#[test]
fn switches_read_from_the_environment() {
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
@@ -322,6 +442,18 @@ mod tests {
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
}
#[test]
fn only_opengl_on_windows_gets_the_system_title_bar() {
use eframe::wgpu::Backends;
assert!(system_title_bar(true, Backends::GL));
for b in [Backends::DX12, Backends::VULKAN, Backends::DX12 | Backends::GL, Backends::VULKAN | Backends::GL, Backends::all()] {
assert!(!system_title_bar(true, b), "{b:?}");
}
for b in [Backends::GL, Backends::VULKAN, Backends::METAL] {
assert!(!system_title_bar(false, b), "{b:?}");
}
}
#[test]
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
use eframe::wgpu::Backends;
+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
@@ -148,6 +150,7 @@ origin false
pan false
params false
paste false
path_flange false
pattern_circ false
pattern_path false
pattern_rect false
@@ -244,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

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Path Flange</title>
<path d="M9.5 44L23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#34465b" stroke-width="11.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.5 44L23.5 44" fill="none" stroke="#b8c3cf" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#328bd3" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

+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;
+17 -1
View File
@@ -335,6 +335,7 @@
"Show Agent Cursor": "Zobrazit kurzor agenta",
"Show in folder": "Zobrazit ve složce",
"Show the pointer an AI agent moves (#35)": "Zobrazit ukazatel, kterým pohybuje AI agent (#35)",
"Show tool at cursor": "Zobrazit nástroj u kurzoru",
"Signed axes": "Osy se znaménkem",
"Size": "Velikost",
"Sketch constraints": "Vazby náčrtu",
@@ -998,6 +999,16 @@
"pattern.direction_1": "Směr 1",
"pattern.direction_2": "Směr 2",
"pattern.flip_direction": "Převrátit směr",
"perf.click_for_details": "Klikněte pro podrobnosti: graf časů snímků, histogram a co vykreslila GPU",
"perf.draw_calls": "Volání vykreslení",
"perf.line_segments": "Úsečky",
"perf.low_1": "1% minimum",
"perf.renderer": "Vykreslování",
"perf.scene_build": "Sestavení scény",
"perf.triangles": "Trojúhelníky",
"perf.ui_time": "Čas UI",
"perf.viewport": "Pohled",
"perf.worst": "nejhorší",
"pick a hole to seat it, or OK places it at the origin": "vyberte otvor pro usazení, nebo OK umístí do počátku",
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
"prefs.cpu": "CPU (softwarově)",
@@ -1222,6 +1233,7 @@
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
"search.valid": "Platné",
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
"settings.frame_rate_details_note": "Přidá graf časů posledních snímků s vykreslenými trojúhelníky, histogram časů snímků a to, co vykreslila GPU. Kliknutím na snímkovou frekvenci v pohledu je zobrazíte nebo skryjete.",
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
@@ -1571,7 +1583,6 @@
"ui.ai_permissions": "Oprávnění",
"ui.ai_preset": "Předvolba",
"ui.ai_remove": "Odebrat",
"ui.ai_request_limit": "zastaveno po 24 požadavcích na jednu zprávu; pro pokračování pošlete další zprávu",
"ui.ai_reset": "Obnovit",
"ui.ai_settings": "Nastavení AI…",
"ui.ai_show_features": "Zobrazit funkce AI",
@@ -1748,6 +1759,8 @@
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
"ui.findInTimeline": "Najít na časové ose",
"ui.findInWindow": "Najít v okně",
"ui.fine": "Jemná",
"ui.finest": "Nejjemnější",
"ui.finish_sketch": "Dokončit náčrt",
"ui.fit": "Přizpůsobit",
"ui.fix_unfix": "Zafixovat/uvolnit",
@@ -1755,6 +1768,7 @@
"ui.flip": "Převrátit",
"ui.format": "Formát",
"ui.frame_rate": "Snímková frekvence",
"ui.frame_rate_details": "Podrobnosti snímkové frekvence",
"ui.front": "Zepředu",
"ui.fully_constrained": "Plně zavazbeno",
"ui.function": "Funkce",
@@ -1974,6 +1988,7 @@
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
"ui.properties": "Vlastnosti",
"ui.pull_direction": "Směr vyjímání",
"ui.quality": "Kvalita",
"ui.quantity": "Počet",
"ui.quantity_2": "Počet 2",
"ui.quit": "Ukončit",
@@ -2068,6 +2083,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Rozdělit těleso",
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
"ui.standard": "Standardní",
"ui.start": "Start",
"ui.start_over": "Začít znovu",
"ui.start_page": "Úvodní stránka",
+390 -7
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,26 +438,35 @@
"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",
"Show in folder": "Vis i mappe",
"Show the pointer an AI agent moves (#35)": "Vis markøren, som en AI-agent bevæger (#35)",
"Show tool at cursor": "Vis værktøj ved markøren",
"Signed axes": "Akser med fortegn",
"Size": "Størrelse",
"Sketch constraints": "Skitsebindinger",
@@ -344,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.",
@@ -356,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å",
@@ -378,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",
@@ -403,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",
@@ -459,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",
@@ -473,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",
@@ -595,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",
@@ -654,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",
@@ -769,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",
@@ -857,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)",
@@ -866,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",
@@ -975,6 +1182,15 @@
"motion.to_end": "Til slutningen",
"motion.to_start": "Til starten",
"motion.track_name": "Spor {number}",
"nav.alias.help": "Shift+Alt+træk med venstre museknap roterer visningen, Shift+Alt+midterste panorerer, Shift+Alt+højre zoomer; træk med midterste museknap panorerer også, og træk med højre roterer visningen",
"nav.blender.help": "Træk med midterste museknap roterer visningen, Shift+midterste panorerer, Ctrl+midterste zoomer; træk med højre museknap påvirker ikke kameraet",
"nav.free_orbit.help": "Musen og 3D-musen roterer gennem begge poler og bevarer rulning. Snap til akser med Shift er slået fra.",
"nav.free_orbit.label": "Fri rotation af visningen",
"nav.fusion.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, træk med højre roterer visningen",
"nav.inventor.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, Ctrl+midterste zoomer, træk med højre roterer visningen",
"nav.openscad.help": "Træk med venstre museknap i et tomt område roterer visningen, Shift+venstre markerer med en ramme, træk i skitsegeometri redigerer den; træk med højre eller midterste museknap panorerer, Shift+højre eller Shift+midterste zoomer",
"nav.solidworks.help": "Træk med midterste museknap roterer visningen, Ctrl+midterste panorerer, Shift+midterste zoomer, træk med højre roterer visningen",
"nav.tinkercad.help": "Træk med højre museknap roterer visningen, træk med midterste eller Shift+højre panorerer",
"nav.touchpad.help": "Hold Shift nede, og bevæg markøren for at panorere; hold Alt/Option nede, og bevæg den for at rotere visningen. Ingen museknap behøver at være trykket ned. Lodret rulning zoomer.",
"nav.touchpad.label": "Pegefelt",
"nav.touchpad.orbit": "Rotér visning (hold Alt/Option nede og bevæg markøren; ingen museknap)",
@@ -997,8 +1213,130 @@
"pattern.flip_direction": "Vend retning",
"pick a hole to seat it, or OK places it at the origin": "vælg et hul til placeringen, eller tryk OK for at placere ved nulpunktet",
"pick one or two vertices, edges or faces": "vælg et eller to hjørnepunkter, kanter eller flader",
"prefs.add_item": "Tilføj elementet",
"prefs.alt_frees_manipulators": "Alt / Option frigør også manipulatorpilene",
"prefs.anti_aliasing_note": "Udjævner takkede kanter.",
"prefs.applies_on_restart": "Træder i kraft efter genstart",
"prefs.assistant": "Assistent",
"prefs.axes": "Akser",
"prefs.axis": "Akse",
"prefs.bottom": "Bund",
"prefs.browser": "Modelbrowser",
"prefs.built_in": "Indbygget",
"prefs.button_left": "Træk med venstre museknap",
"prefs.button_middle": "Træk med midterste museknap",
"prefs.button_right": "Træk med højre museknap",
"prefs.choose_font": "Vælg skrifttypefil…",
"prefs.click": "Klik",
"prefs.contact_shading": "Blød kontaktskygning",
"prefs.contact_shading_note": "Blød skygning, hvor dele er tæt på hinanden.",
"prefs.context_menu": "Genvejsmenu",
"prefs.context_menu_ring": "Genvejsmenu · hold nede for at åbne den radiale menu",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Åbn nedbrudsrapporter…",
"prefs.crash_reports_note": "Rapporter gemmes på denne computer med designet og de kommandoer, der genskaber problemet.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Krumme overflader",
"prefs.curves": "Krumme overflader",
"prefs.dead_zone_note": "Det skraverede bånd er dødzonen: Bevægelser inden for det ignoreres.",
"prefs.default": "standard",
"prefs.design": "Standardindstillinger for design",
"prefs.design_note": "Udgangspunktet for nye design. Åbne design beholder deres egne indstillinger.",
"prefs.devices": "3D-mus",
"prefs.display": "Visning",
"prefs.edges": "Kanter og skygning",
"prefs.extend": "Udvid",
"prefs.extra_navigation": "Ctrl+træk med venstre museknap i et tomt område roterer visningen; hold mellemrumstasten nede for at panorere.",
"prefs.follows_os": "Følger operativsystemet",
"prefs.font": "Skrifttype",
"prefs.font_fallback": "Tegn, som den valgte skrifttype mangler, vises med den indbyggede skrifttype, så ingen tekst viser en tom firkant.",
"prefs.font_sample": "Høj bly gom vandt fræk sexquiz · Ø12,50 mm · 45,0° · 0123456789",
"prefs.font_unreadable": "Den valgte skrifttype kunne ikke læses.",
"prefs.general": "Generelt",
"prefs.general_note": "Sprog, lagring og opdateringer.",
"prefs.gesture_orbit": "Rotér visningen",
"prefs.gesture_pan": "Panorér",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Grafik",
"prefs.graphics_note": "Afvej hastighed mod udjævning. Hvert valg viser sin virkning på et eksempel.",
"prefs.graphics_presets_note": "Forudindstillinger angiver de nedenstående valg samlet.",
"prefs.history": "Historik",
"prefs.hold_stop_snapping": "Hold nede for at slå snapping fra",
"prefs.icons_only": "Kun ikoner",
"prefs.imported_file": "Importeret fil",
"prefs.input": "Input",
"prefs.interface": "Brugergrænseflade",
"prefs.interface_language": "Sprog i brugergrænsefladen",
"prefs.interface_note": "SolidCrafts udseende og placeringen af dets dele.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Etiketter",
"prefs.layout": "Layout",
"prefs.layout_note": "Kompakt fastgør panelerne til vinduets kanter. Gennemsigtighed i modelbrowseren er tilgængelig i Flydende. Skift assistentens side på siden Assistent.",
"prefs.left": "Venstre",
"prefs.left_click": "Venstreklik",
"prefs.live_input": "Løbende input",
"prefs.live_preview": "Løbende forhåndsvisning",
"prefs.minutes": "minutter",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mus og pegefelt",
"prefs.mouse_heading": "Mus",
"prefs.mouse_mapping": "Musetildeling",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "Hvordan musen, pegefeltet og 3D-musen styrer visningen.",
"prefs.not_on_gpu": "ikke på denne GPU",
"prefs.on": "Til",
"prefs.opaque": "Uigennemsigtig",
"prefs.orbit_snap": "Hold Shift nede under rotation af visningen for at snappe til en akse.",
"prefs.orientation": "Orientering",
"prefs.overall_speed": "Samlet hastighed",
"prefs.preview_choices": "Forhåndsvisning af dine valg",
"prefs.quality_default": "standard",
"prefs.quality_fastest": "hurtigst",
"prefs.quality_slowest": "langsomst",
"prefs.quality_smoother": "mere udjævnet",
"prefs.reports_saved": "{n} gemt",
"prefs.right_click": "Højreklik",
"prefs.saving": "Automatisk lagring og gendannelse",
"prefs.scale_note": "Også Ctrl + / Ctrl − i vinduet. Knapper er 28 px ved 100%.",
"prefs.scale_text": "Skalering og tekst",
"prefs.select": "Vælg",
"prefs.select_one_item": "Vælg ét element",
"prefs.selecting": "Markering",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift tilføjer kun; Ctrl / Cmd skifter markeringen",
"prefs.shift_add_note": "Til: Shift tilføjer, Ctrl / Cmd skifter markeringen. Fra (standard): Begge skifter markeringen.",
"prefs.shortcuts": "Genveje",
"prefs.sketch_selection": "Skitse og markering",
"prefs.sketch_selection_heading": "Skitse og markering",
"prefs.sketch_selection_note": "Hvordan skitser snapper, og hvordan klik markerer.",
"prefs.sketching": "Skitsering",
"prefs.snap_drawing_note": "Mens du tegner eller trækker et skitsepunkt.",
"prefs.source": "Kilde",
"prefs.speed": "Hastighed",
"prefs.stand_up_note": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X.",
"prefs.status_done": "Færdig",
"prefs.status_error": "Fejl",
"prefs.status_selected": "Valgt",
"prefs.status_warning": "Advarsel",
"prefs.supersampling_brief": "Tegner visningen i større størrelse og nedskalerer den med filtrering: meget skarpt og mere krævende.",
"prefs.tab_accent_note": "Accentfarven følger den aktive fane",
"prefs.theme_file": "temafil",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "nu",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Tilføj elementet, eller fjern det igen",
"prefs.toolbar": "Værktøjslinje",
"prefs.translucent": "Halvgennemsigtig",
"prefs.under_browser": "Under modelbrowseren",
"prefs.under_cursor": "Under markøren",
"prefs.units_precision": "Enheder og præcision",
"prefs.updates": "Opdateringer",
"prefs.view": "Visningens adfærd",
"prefs.viewport": "Visningsområde",
"prefs.wheel": "Musehjul",
"prefs.wheel_trackpad": "Musehjul og pegefelt",
"prefs.workspace": "Arbejdsområde",
"prefs.zoom_toward_cursor": "Zoom mod markøren",
"properties.title": "Egenskaber: {name}",
"recovery.days_ago": "for {count} dage siden",
"recovery.detail": "{file} · {features} features · autogemt {when}",
@@ -1069,16 +1407,20 @@
"search.suggest": "Mente du {label}?",
"search.try": "Skriv et kommandonavn, en feature i dette design eller en parameter. Du kører en kommando med værdier ved at skrive dens id og et JSON-objekt: {example}",
"search.valid": "Gyldig",
"settings.display_quality_note": "Finere niveauer viser krumme flader og kanter mere jævnt og tager længere tid at opbygge mesh til. Kun modelvisningen ændres, aldrig designet eller eksporten.",
"section.off_hint": "Intet plan valgt: OK slår snittet fra.",
"settings.display_quality_note": "Gælder kun visningen. Design og eksport påvirkes ikke.",
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen.",
"settings.measure_axis_distances": "Vis X/Y/Z-afstande",
"settings.measure_axis_distances_help": "Vis afstanden langs X-, Y- og Z-aksen sammen med den samlede afstand i Mål.",
"settings.msaa_applies_on_restart": "Kantudjævning træder i kraft, næste gang SolidCraft starter",
"settings.msaa_reset": "{setting}: {requested}× understøttes ikke af denne grafikenhed; værdien er nu {reset}× (understøttet: {supported}).",
"settings.msaa_unsupported": "Understøttes ikke af denne grafikenhed",
"settings.reference_geometry_through_bodies": "Vis referencegeometri gennem legemer",
"settings.reference_geometry_through_bodies_help": "Tegn inaktive skitsekonturer og referenceplaner oven på skyggelagte legemer",
"settings.reset_intro": "Disse indstillinger virker ikke på denne computer og ville forhindre SolidCraft i at starte. De er derfor ændret:",
"settings.reset_title": "Indstillinger blev ændret",
"settings.supersampling_note": "Tegner visningen med højere opløsning og nedskalerer den: Jævnere kanter, linjer og gitter på skærme under 4K. Det kræver GPU-tid: ca. 2,25× så mange pixels ved 1,5× og 4× ved 2×.",
"sheet.no_body": "der er intet pladelegeme: Start et med Flange > Basis, eller omdan en plade til et pladelegeme med Konvertér til plade (vælg dens store plane flade)",
"shortcut.json_error": "parametre skal være JSON: {error}",
"shortcut.no_command": "ingen kommando `{id}`; teksten før JSON skal være et kommando-id",
"shortcut.pin": "Fastgør øverst i S-boksen",
@@ -1115,6 +1457,9 @@
"thread.designation": "Betegnelse",
"thread.iso_metric": "ISO-metrisk",
"thread.select_size": "Vælg størrelse",
"timeline.delete_after_marker_one": "Slet 1 feature efter historikmarkøren",
"timeline.delete_after_marker_other": "Slet {count} features efter historikmarkøren",
"timeline.delete_many": "Slet {count} valgte features",
"timeline.group": "Ny featuremappe",
"timeline.move_many": "Flyt elementer i tidslinjen",
"timeline.move_to_group": "Flyt features til mappe",
@@ -1360,11 +1705,15 @@
"ui.agent_chat_waiting": "Venter på dig",
"ui.agent_cursor": "Agentmarkør",
"ui.agent_cursor_speed": "Agentmarkørens hastighed",
"ui.agent_off_thread": "Hold programvinduet responsivt, mens agenter udfører kommandoer",
"ui.agent_off_thread_note": "Langsomme kommandoer fra en agent (via AI-panelet eller en MCP-klient) køres i en separat tråd, så du stadig kan rotere og undersøge modellen. Du kan stoppe en kommando via statusfeltet i visningen. Når funktionen er slået fra, køres kommandoerne på programvinduets tråd, og vinduet venter, indtil de er færdige.",
"ui.ai_add": "Tilføj",
"ui.ai_add_agent": "Tilføj agent…",
"ui.ai_add_agent_title": "Tilføj agent",
"ui.ai_add_anthropic": "Anthropic-kompatibel API",
"ui.ai_add_api": "OpenAI-kompatibel API",
"ui.ai_add_custom": "Brugerdefineret kommando",
"ui.ai_add_endpoint": "API-slutpunkt",
"ui.ai_add_installed": "Installeret agent",
"ui.ai_add_installed_note": "Din egen opsætning af en installeret agent, f.eks. med andre argumenter. Installerede agenter vises automatisk.",
"ui.ai_added": "tilføjet",
@@ -1393,6 +1742,7 @@
"ui.ai_key_needs_https": "En nøgle sendes kun via HTTPS eller til denne computer.",
"ui.ai_key_note": "Gemmes i systemets nøglering, aldrig i SolidCrafts indstillinger. Lad feltet være tomt ved en lokal server.",
"ui.ai_keychain_error": "Systemets nøglering afviste anmodningen: {error}",
"ui.ai_kind": "Type",
"ui.ai_mode_all": "Tillad alt",
"ui.ai_mode_all_note": "Intet kræver godkendelse først. Nedbrudsrapporter er stadig utilgængelige, og mappebegrænsningerne gælder fortsat.",
"ui.ai_mode_all_warning": "Agenten kan gemme, overskrive, eksportere og erstatte dit design uden at spørge. Brug kun dette med en agent, du har tillid til, og på designs, du har gemt.",
@@ -1414,8 +1764,8 @@
"ui.ai_panel": "Panel",
"ui.ai_permissions": "Tilladelser",
"ui.ai_preset": "Forudindstilling",
"ui.ai_protocol": "Protokol",
"ui.ai_remove": "Fjern",
"ui.ai_request_limit": "stoppet efter 24 anmodninger for én besked; send en ny besked for at fortsætte",
"ui.ai_reset": "Nulstil",
"ui.ai_settings": "AI-indstillinger…",
"ui.ai_show_features": "Vis AI-funktioner",
@@ -1424,6 +1774,8 @@
"ui.ai_test": "Test",
"ui.ai_test_acp_ok": "Svarer: {agent}",
"ui.ai_test_acp_tip": "Starter agenten kortvarigt og anmoder om en session; afslutter den derefter igen",
"ui.ai_test_answered": "Forbindelse oprettet: {model} svarede",
"ui.ai_test_anthropic_tip": "Sender en kort besked til slutpunktet",
"ui.ai_test_api_tip": "Henter endepunktets modeller",
"ui.ai_test_no_model": "Forbindelse oprettet: {count} modeller, men ikke {model}",
"ui.ai_test_ok": "Forbindelse oprettet: {count} modeller",
@@ -1433,8 +1785,8 @@
"ui.ai_tool_running": "kører",
"ui.ai_tool_unfold": "Klik for at vise svaret",
"ui.ai_tool_waiting": "venter",
"ui.ai_toolbar_button": "Vis knap på værktøjslinjen",
"ui.ai_toolbar_button_note": "AI-knappen i højre ende af værktøjslinjen. Ctrl+Shift+A og menuen Vis åbner stadig panelet.",
"ui.ai_toolbar_button": "Vis Assistent-knappen på programlinjen",
"ui.ai_toolbar_button_note": "Assistent-knappen på programlinjen. Du kan stadig åbne panelet med Ctrl+Shift+A eller via menuen Vis.",
"ui.ai_undo_tip": "Fortryd alt, agenten ændrede for denne besked, som ét trin",
"ui.ai_url_hint": "https://example.com/v1",
"ui.all": "Alle",
@@ -1582,7 +1934,12 @@
"ui.export_keymap_tip": "Gem tastaturgenveje, favoritter i S-menuen og værktøjslinjen i en fil",
"ui.extend": "Forlæng",
"ui.extent_type": "Udstrækningstype",
"ui.extract_component": "Udtræk til fil…",
"ui.extract_design_changed": "Designet blev ændret, mens du valgte en fil. Start Udtræk til fil igen.",
"ui.extract_done": "Komponenten er nu et selvstændigt design, der er linket hertil.",
"ui.extrude": "Ekstrudering",
"ui.extrude_auto_to_object": "Ekstrudér til et valgt mål",
"ui.extrude_auto_to_object_tip": "Når en profil er valgt i Ekstrudering, skifter udstrækningen til Til objekt, hvis du vælger en flade, et hjørnepunkt eller et parallelt plan uden for profilens plan",
"ui.factor": "Faktor",
"ui.feature_history": "Featuretidslinje",
"ui.feature_options": "Featureindstillinger",
@@ -1592,6 +1949,8 @@
"ui.filter_commands_or_keys": "Filtrér kommandoer eller taster",
"ui.findInTimeline": "Find i tidslinje",
"ui.findInWindow": "Find i vindue",
"ui.fine": "Fin",
"ui.finest": "Fineste",
"ui.finish_sketch": "Afslut skitse",
"ui.fit": "Tilpas",
"ui.fix_unfix": "Lås fast/frigør",
@@ -1689,10 +2048,17 @@
"ui.hideProfile": "Skjul profil",
"ui.history_back": "Gå ét trin tilbage",
"ui.history_below_browser": "Under browseren",
"ui.history_edge_hint": "dobbeltklik på kanten",
"ui.history_first": "Gå til begyndelsen",
"ui.history_forward": "Gå ét trin frem",
"ui.history_hide_names": "Skjul navne",
"ui.history_last": "Gå til slutningen",
"ui.history_marker_tip": "Historikmarkør: Træk for at rulle modellen tilbage; nye features indsættes her",
"ui.history_names": "Navne",
"ui.history_play": "Afspil til slutningen",
"ui.history_show_names": "Vis navne",
"ui.history_title": "Historik",
"ui.history_toggle_names": "Vis eller skjul featurenavne",
"ui.hole": "Hul",
"ui.hole_tap_type": "Hullets gevindtype",
"ui.hole_type": "Hultype",
@@ -1714,6 +2080,10 @@
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
"ui.link_copies": "Link identiske kopier",
"ui.link_copies_keep": "Behold",
"ui.link_copies_link": "Link",
"ui.link_copies_text": "Disse komponenter danner de samme legemer som {name}. Skal de gøres til placeringer af dens link?",
"ui.lock": "Lås",
"ui.loft": "Loft",
"ui.look_at": "Se på",
@@ -1816,6 +2186,7 @@
"ui.project_face_edges_sketch": "Projektér fladekanter til nye skitser",
"ui.properties": "Egenskaber",
"ui.pull_direction": "Udtræksretning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Afslut",
@@ -1827,6 +2198,9 @@
"ui.rectangle": "Rektangel",
"ui.rectangular_pattern": "Rektangulært mønster",
"ui.redo": "Gentag",
"ui.reference_1": "Reference 1",
"ui.reference_2": "Reference 2",
"ui.reference_distance": "Referenceafstand",
"ui.relink_design_changed": "Samlingen blev ændret, mens du valgte en kilde. Start Skift kilde igen.",
"ui.relink_done": "Kilden er ændret for alle forekomster.",
"ui.relink_not_linked": "Denne komponent er ikke knyttet til et kildedesign.",
@@ -1837,6 +2211,7 @@
"ui.render": "Rendering",
"ui.renderer": "Renderingsmotor",
"ui.repeat": "Gentag",
"ui.replace_with_link_done": "Kopierne er nu placeringer af det linkede design.",
"ui.report_a_bug": "Rapportér en fejl",
"ui.reset": "Nulstil",
"ui.reset_all": "Nulstil alle",
@@ -1875,6 +2250,12 @@
"ui.sent_to_slicer": "Sendt til {slicer}",
"ui.sheet_gap": "Mellemrum",
"ui.shell": "Udhulning",
"ui.shortcut_capture_add_key": "Tilføj én almindelig tast. Escape annullerer.",
"ui.shortcut_capture_hint": "Klik på en tastetildeling, og tryk derefter på modifikationstaster og én tast. Tilføj modifikationstaster inden for ét sekund; slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_press": "Tryk på modifikationstaster og én tast. Escape annullerer.",
"ui.shortcut_capture_release": "Slip alle taster for at bekræfte. Escape annullerer.",
"ui.shortcut_capture_settling": "Bekræfter genvej… Escape annullerer.",
"ui.shortcut_multiple_keys": "Brug modifikationstaster og kun én anden tast. Klik på tastetildelingen for at prøve igen.",
"ui.show": "Vis",
"ui.showAll": "Vis alle",
"ui.showAllBodies": "Vis alle emner",
@@ -1910,6 +2291,7 @@
"ui.spline": "Spline",
"ui.split_body": "Opdel emne",
"ui.stand_up_tip": "Når en STEP- eller IGES-fil åbnes med Z opad, tilbydes en kvart omdrejning om X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Start forfra",
"ui.start_page": "Startside",
@@ -1937,7 +2319,7 @@
"ui.tie_ends": "Sammenbind",
"ui.tie_ends_tip": "Bind alle ubundne ender",
"ui.timeline_bottom": "Tidslinje nederst",
"ui.timeline_left_note": "Layoutet til venstre viser tilbagerulningslinjen over modeltræet med emner og scene.",
"ui.timeline_left_note": "Den lodrette historik vises til venstre for modelbrowseren. Featurenavne kan vises eller skjules på strimlen.",
"ui.timeline_left_sidebar": "Venstre sidepanel",
"ui.tip_angle": "Spidsvinkel",
"ui.to_object": "Til objekt",
@@ -1945,7 +2327,7 @@
"ui.top": "Ovenfra",
"ui.torus": "Torus",
"ui.total_angle": "Samlet vinkel",
"ui.trackpad_pan_tip": "Bevægelser på pegefeltet panorerer (slået fra: zoomer som musehjulet); musehjulet zoomer, tommelfingerhjulet panorerer sidelæns, og knibebevægelse zoomer altid",
"ui.trackpad_pan_tip": "Rulning med to fingre panorerer; i Fusion roterer Shift + to fingre visningen. Fra: Rulning zoomer. Musehjul og knibebevægelser zoomer; tommelfingerhjulet panorerer sidelæns.",
"ui.trim": "Trim",
"ui.twist": "Vridning",
"ui.two_finger_scroll_pans": "Rulning med to fingre panorerer",
@@ -2022,5 +2404,6 @@
"update.up_to_date": "SolidCraft er opdateret.",
"update.update_now": "Opdatér nu",
"update.you_have": "Du har {version}.",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk"
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ vælg · Enter / Tab udfyld · Esc luk",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ vælg · Enter anvender det indtastede · Esc lukker"
}
+9 -1
View File
@@ -421,6 +421,7 @@
"Show Agent Cursor": "Agenten-Cursor anzeigen",
"Show in folder": "Im Ordner anzeigen",
"Show the pointer an AI agent moves (#35)": "Den Zeiger anzeigen, den ein KI-Agent bewegt (#35)",
"Show tool at cursor": "Werkzeug am Cursor anzeigen",
"Signed axes": "Vorzeichenbehaftete Achsen",
"Size": "Größe",
"Sketch constraints": "Skizzenbedingungen",
@@ -1362,6 +1363,7 @@
"thread.designation": "Bezeichnung",
"thread.iso_metric": "ISO metrisch",
"thread.select_size": "Größe wählen",
"timeline.feature_hint": "Doppelklicken zum Bearbeiten · Ziehen zum Neuordnen · Rechtsklicken für weitere Optionen",
"timeline.group": "Neuen Merkmalsordner anlegen",
"timeline.move_many": "Zeitleistenelemente verschieben",
"timeline.move_to_group": "Merkmale in Ordner verschieben",
@@ -1666,7 +1668,6 @@
"ui.ai_permissions": "Berechtigungen",
"ui.ai_preset": "Vorlage",
"ui.ai_remove": "Entfernen",
"ui.ai_request_limit": "nach 24 Anfragen für eine Nachricht angehalten; senden Sie eine weitere Nachricht, um fortzufahren",
"ui.ai_reset": "Zurücksetzen",
"ui.ai_settings": "KI-Einstellungen…",
"ui.ai_show_features": "KI-Funktionen anzeigen",
@@ -1846,6 +1847,8 @@
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
"ui.findInTimeline": "In der Zeitleiste suchen",
"ui.findInWindow": "Im Fenster suchen",
"ui.fine": "Fein",
"ui.finest": "Sehr fein",
"ui.finish_sketch": "Skizze beenden",
"ui.fit": "Einpassen",
"ui.fix_unfix": "Fixieren/Lösen",
@@ -2028,6 +2031,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",
@@ -2074,6 +2078,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",
@@ -2175,6 +2180,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",
@@ -2228,6 +2234,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",
@@ -2277,6 +2284,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",
+120 -1
View File
@@ -8,6 +8,7 @@
"A Single hole snaps to the middle of its face and to the centres of its round edges": "A Single hole snaps to the middle of its face and to the centres of its round edges",
"A new configuration, a copy of the active one": "A new configuration, a copy of the active one",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out",
"A short title for the problem (optional)": "A short title for the problem (optional)",
"ARRANGE": "ARRANGE",
"About SolidCraft": "About SolidCraft",
"Acceleration": "Acceleration",
@@ -21,6 +22,7 @@
"Add Mid Radius": "Add Mid Radius",
"Add Row": "Add Row",
"Add a tool": "Add a tool",
"Add bend": "Add bend",
"Add condition": "Add condition",
"Add or edit rule": "Add or edit rule",
"Add to New Folder": "Add to New Folder",
@@ -35,7 +37,9 @@
"Also deletes": "Also deletes",
"Anchor": "Anchor",
"Angle 2": "Angle 2",
"Angle Display": "Angle Display",
"Animate": "Animate",
"Another bend on the far edge of the last flange": "Another bend on the far edge of the last flange",
"App/system information and command settings are included. Review private details before sharing.": "App/system information and command settings are included. Review private details before sharing.",
"Appearance name": "Appearance name",
"Applied Radius": "Applied Radius",
@@ -52,6 +56,7 @@
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
"BROWSER": "BROWSER",
"Bar": "Bar",
"Bend": "Bend",
"Bend Edges": "Bend Edges",
"Bend Line Position": "Bend Line Position",
"Bend Position": "Bend Position",
@@ -138,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)",
@@ -218,6 +224,7 @@
"Fit and G2 corners require Constant radius.": "Fit and G2 corners require Constant radius.",
"Fixed Face": "Fixed Face",
"Flange thickness": "Flange thickness",
"Flanges": "Flanges",
"Flat Pattern": "Flat Pattern",
"Flat size": "Flat size",
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
@@ -227,11 +234,15 @@
"From Bodies": "From Bodies",
"From edges": "From edges",
"Full Length": "Full Length",
"Full thread depth": "Full thread depth",
"Further Bends": "Further Bends",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.",
"Gap": "Gap",
"Grid": "Grid",
"HISTORY": "HISTORY",
"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",
@@ -247,6 +258,8 @@
"In This Design": "In This Design",
"In place (surfaces)": "In place (surfaces)",
"In plane": "In plane",
"Included": "Included",
"Inner Sharp": "Inner Sharp",
"Inside": "Inside",
"Instant": "Instant",
"Interface font": "Interface font",
@@ -293,6 +306,7 @@
"Mass Center": "Mass Center",
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
"Material": "Material",
"Material Outside": "Material Outside",
"Material copied. Existing body assignments are unchanged.": "Material copied. Existing body assignments are unchanged.",
"Max": "Max",
"Maximum": "Maximum",
@@ -332,14 +346,17 @@
"Nominal diameter": "Nominal diameter",
"None": "None",
"Not available yet": "Not available yet",
"Not saved yet": "Not saved yet",
"Notch": "Notch",
"OUTPUT": "OUTPUT",
"Of": "Of",
"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.",
@@ -347,6 +364,8 @@
"Orbit (right drag)": "Orbit (right drag)",
"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",
@@ -402,6 +421,7 @@
"Remove screenshot": "Remove screenshot",
"Remove the column": "Remove the column",
"Remove the track": "Remove the track",
"Remove this bend": "Remove this bend",
"Remove this point": "Remove this point",
"Rename Feature": "Rename Feature",
"Rename Folder": "Rename Folder",
@@ -443,6 +463,7 @@
"Rules run from top to bottom. Matched drives change effective values.": "Rules run from top to bottom. Matched drives change effective values.",
"SECTION": "SECTION",
"SURFACE QUALITY": "SURFACE QUALITY",
"Same way": "Same way",
"Save Rule": "Save Rule",
"Save View": "Save View",
"Save screenshot…": "Save screenshot…",
@@ -471,8 +492,10 @@
"Shape": "Shape",
"Sheet Rules": "Sheet Rules",
"Show Agent Cursor": "Show Agent Cursor",
"Show ghost": "Show ghost",
"Show in folder": "Show in folder",
"Show the pointer an AI agent moves (#35)": "Show the pointer an AI agent moves (#35)",
"Show tool at cursor": "Show tool at cursor",
"Signed axes": "Signed axes",
"Size": "Size",
"Sketch constraints": "Sketch constraints",
@@ -509,9 +532,11 @@
"Surface, or faces to turn over": "Surface, or faces to turn over",
"Swap Ends": "Swap Ends",
"TRIM": "TRIM",
"Tangent": "Tangent",
"Tangent (G1)": "Tangent (G1)",
"Tangent Chain": "Tangent Chain",
"Tangent chain": "Tangent chain",
"Tangent to Edge": "Tangent to Edge",
"Tap drill diameter": "Tap drill diameter",
"Target": "Target",
"Target Body": "Target Body",
@@ -529,6 +554,7 @@
"This child is shared. Position changes apply to every copy of its parent.": "This child is shared. Position changes apply to every copy of its parent.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.",
"Thread depth": "Thread depth",
"Thread diameter": "Thread diameter",
"To": "To",
"To Offset": "To Offset",
@@ -588,10 +614,13 @@
"Z angle": "Z angle",
"Z distance": "Z distance",
"Zebra": "Zebra",
"ai.connected_toast": "SolidCraft is now available in {clients}. Restart them to use it (Settings > Assistant).",
"ai.prefs.allow_control": "Let AI agents connect (control channel / MCP)",
"ai.prefs.allow_control_note": "Opens a control channel on this computer only (127.0.0.1, port 27878 or the next free one). A program that connects must also show a secret token that only your user account can read, made fresh at every start. Takes effect the next time SolidCraft starts. Off: nothing listens unless you start SolidCraft with --control.",
"ai.prefs.availability": "Assistant controls",
"ai.prefs.background_commands": "Run agent commands in the background",
"ai.prefs.connect_apps": "Make SolidCraft available to AI apps on this computer",
"ai.prefs.connect_apps_note": "When SolidCraft starts, adds it as an MCP server to the AI apps found on this computer (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code), so an agent can use it without any setup. Only the solidcraft entry is added or its path refreshed; an entry you changed is left alone, and the file is backed up first. Off: the entries it added are removed. Help > Connect AI assistants… does this by hand.",
"ai.prefs.follow_camera": "Turn the camera to sketches",
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
@@ -663,6 +692,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",
@@ -747,6 +777,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",
@@ -756,6 +788,7 @@
"cmd.inspect_interference": "Interference",
"cmd.inspect_isocurve": "Isocurve Analysis",
"cmd.inspect_minimum_radius": "Minimum Radius Analysis",
"cmd.inspect_parameter_sweep": "Parameter Sweep",
"cmd.inspect_silhouette": "Silhouette",
"cmd.inspect_zebra": "Zebra Analysis",
"cmd.joint_as_built": "As-Built Joint",
@@ -881,6 +914,7 @@
"cmd.sheet_fold": "Fold",
"cmd.sheet_hem": "Hem",
"cmd.sheet_manage_rules": "Sheet Metal Rules",
"cmd.sheet_path_flange": "Path Flange",
"cmd.sheet_perforate": "Perforate",
"cmd.sheet_refold": "Refold",
"cmd.sheet_rules": "List Sheet Metal Rules",
@@ -1058,6 +1092,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",
@@ -1073,7 +1108,18 @@
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
"dialog.selection_step": "{step} of {total}: Select {input}",
"dialog.step": "Step",
"document.changed_on_disk": "{name} changed on disk",
"document.changed_on_disk_detail": "{name} was changed by another program or computer since you opened or last saved it. Overwrite replaces those changes with yours; Save a Copy keeps both.",
"document.copy_name": "{name} copy",
"document.open_anyway": "Open Anyway",
"document.open_elsewhere": "{name} is already open",
"document.open_elsewhere_detail": "{name} is open in SolidCraft {app} on {who}, opened {when}. If you both save, one of you loses work. Open it read-only to look at it without saving over it.",
"document.open_read_only": "Open Read-Only",
"document.opened_read_only": "Opened read-only: Save asks for a new file",
"document.overwrite": "Overwrite",
"document.read_only_save": "This design was opened read-only: save it as a new file",
"document.save_changes": "Save changes to {name}?",
"document.save_copy": "Save a Copy…",
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
"drive.stopped_by_contact": "Stopped at {value}: {a} would pass through {b}",
"drive.stopped_by_loop": "Stopped at {value}: the loop through {joint} can't close any further",
@@ -1090,9 +1136,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...",
@@ -1102,6 +1150,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",
@@ -1109,17 +1158,25 @@
"help.apache": "Apache licence 2.0",
"help.attributions": "Attributions (ATTRIBUTION.md)",
"help.build": "Version {version} · commit {commit} · built {date}",
"help.connect_note": "Restart each assistant to load the SolidCraft server. Each file was backed up as .bak before it changed.",
"help.connect_title": "Connect AI assistants",
"help.font_details": "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans (SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.",
"help.fonts": "Fonts",
"help.licences": "Licences",
"help.mit": "MIT licence",
"help.notice": "NOTICE",
"help.solid.chamfer": "Cuts the selected edges with a flat bevel of the distance you set. Use it to break sharp edges or to ease the entry of a pin or screw.",
"help.solid.combine": "Joins bodies, cuts them out of a target body, or keeps only the volume they share, and can keep the tool bodies. Use it to merge parts, to cut one shape with another or to find where two bodies overlap.",
"help.solid.extrude": "Pushes a sketch profile into 3D along a distance, adding or removing material. Use it to turn a flat outline into a block, a boss or a pocket.",
"help.solid.fillet": "Rounds the selected edges with a smooth circular blend of the radius you set. Use it to soften sharp edges for strength, comfort or looks.",
"help.solid.hole": "Drills a round hole of the diameter you set at a chosen point on a face. Use it for screw clearances, pins and other simple round openings.",
"help.solid.loft": "Blends a smooth solid between two or more sketch profiles, or from a profile to a point. Use it for transitions, funnels, handles and any shape that changes section along its length.",
"help.solid.mirror": "Makes a mirrored copy of the selected bodies or features across a plane, and can merge it with the original. Use it for symmetric parts, so that you model one half and get both.",
"help.solid.pattern.circular": "Repeats the selected objects around an axis, for the count and total angle you set. Use it for bolt circles, spokes, fins and teeth.",
"help.solid.pattern.rectangular": "Repeats the selected objects in one or two directions with the count and spacing you set. Use it for rows and grids of holes, ribs, pins or feet.",
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
"help.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",
"home.back": "Back to {name}",
"home.crashes": "Crash reports",
@@ -1196,6 +1253,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",
@@ -1301,10 +1360,22 @@
"pattern.direction_1": "Direction 1",
"pattern.direction_2": "Direction 2",
"pattern.flip_direction": "Flip direction",
"perf.click_for_details": "Click for details: frame-time graph, histogram and what the GPU drew",
"perf.draw_calls": "Draw calls",
"perf.line_segments": "Line segments",
"perf.low_1": "1% low",
"perf.renderer": "Renderer",
"perf.scene_build": "Scene build",
"perf.triangles": "Triangles",
"perf.ui_time": "UI time",
"perf.viewport": "Viewport",
"perf.worst": "worst",
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
"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",
@@ -1348,6 +1419,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",
@@ -1379,6 +1454,7 @@
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
"prefs.orientation": "Orientation",
"prefs.overall_speed": "Overall speed",
"prefs.prefer_free_sketch_points": "Prefer movable sketch points when points overlap",
"prefs.preview_choices": "Preview of your choices",
"prefs.quality_default": "default",
"prefs.quality_fastest": "fastest",
@@ -2052,7 +2128,9 @@
"search.try": "Type a command name, a feature in this design, or a parameter. To run a command with values, type its id and a JSON object: {example}",
"search.valid": "Valid",
"section.off_hint": "No plane picked: OK turns the section off.",
"set by the sketch's lines": "set by the sketch's lines",
"settings.display_quality_note": "Only the display. Designs and exports are not affected.",
"settings.frame_rate_details_note": "Adds a graph of the last frames' times with the triangles drawn over it, a histogram of frame times and what the GPU drew. Click the frame rate in the view to show or hide them.",
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner.",
"settings.measure_axis_distances": "Show X/Y/Z distances",
"settings.measure_axis_distances_help": "Show world-axis component dimensions alongside the total distance while Measure is open.",
@@ -2083,6 +2161,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.",
@@ -2107,6 +2190,7 @@
"timeline.delete_after_marker_one": "Delete 1 Feature After History Marker",
"timeline.delete_after_marker_other": "Delete {count} Features After History Marker",
"timeline.delete_many": "Delete {count} Selected Features",
"timeline.feature_hint": "Double-click to edit · drag to reorder · right-click for more",
"timeline.group": "New Feature Folder",
"timeline.move_many": "Move Timeline Items",
"timeline.move_to_group": "Move Features to Folder",
@@ -2254,7 +2338,9 @@
"ui.agent_chat_auth_title": "Sign in to the agent",
"ui.agent_chat_cancel": "Cancel",
"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",
@@ -2305,6 +2391,7 @@
"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.",
@@ -2313,6 +2400,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",
@@ -2330,6 +2418,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",
@@ -2358,6 +2449,7 @@
"ui.agent_chat_stop_tip": "Stop the agent and everything it started",
"ui.agent_chat_stop_turn_tip": "Stop the agent (Esc). Queued messages stay queued.",
"ui.agent_chat_turn_limit": "Changes per message",
"ui.agent_chat_turn_unlimited": "Unlimited",
"ui.agent_chat_undo_changes": "Undo these changes",
"ui.agent_chat_undo_label": "Agent chat: {message}",
"ui.agent_chat_undone": "(undone)",
@@ -2426,7 +2518,9 @@
"ui.ai_preset": "Preset",
"ui.ai_protocol": "Protocol",
"ui.ai_remove": "Remove",
"ui.ai_request_limit": "stopped after 24 requests for one message; send another message to go on",
"ui.ai_request_timed_out": "The model took longer than {seconds} s. You can raise the request timeout for this endpoint in Preferences › Assistant.",
"ui.ai_request_timeout": "Request timeout",
"ui.ai_request_timeout_hint": "How long one model request to this endpoint may take, from 30 s to an hour. Raise it for a slow local model.",
"ui.ai_reset": "Reset",
"ui.ai_settings": "AI settings…",
"ui.ai_show_features": "Show AI features",
@@ -2534,6 +2628,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",
@@ -2610,6 +2706,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",
@@ -2617,6 +2715,7 @@
"ui.flip": "Flip",
"ui.format": "Format",
"ui.frame_rate": "Frame rate",
"ui.frame_rate_details": "Frame rate details",
"ui.front": "Front",
"ui.fully_constrained": "Fully constrained",
"ui.function": "Function",
@@ -2802,6 +2901,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",
@@ -2821,6 +2921,11 @@
"ui.paste": "Paste",
"ui.path": "Path",
"ui.pattern_on_path": "Pattern on Path",
"ui.perforate_margin_default": "from the diameter",
"ui.perforate_pitch": "Pitch",
"ui.perforate_square": "Square",
"ui.perforate_staggered": "Staggered (60°)",
"ui.perforate_title": "PERFORATE",
"ui.perpendicular": "Perpendicular",
"ui.perspective_view": "Perspective view",
"ui.pinToShortcuts": "Pin to Shortcuts",
@@ -2832,7 +2937,9 @@
"ui.plugins": "Plugins",
"ui.plugins_desktop": "Plugins are available in the desktop app.",
"ui.plugins_empty": "No plugins installed.",
"ui.plugins_none": "No plugins match this search.",
"ui.plugins_note": "Each plugin is a folder inside {path} with a plugin.json. SolidCraft reads that file and does not run any code from the folder. Turning a plugin off keeps it installed.",
"ui.plugins_search": "Search plugins",
"ui.plugins_skipped": "{count} folder(s) could not be read.",
"ui.plugins_version": "{version}",
"ui.point": "Point",
@@ -2848,6 +2955,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",
@@ -2954,9 +3062,12 @@
"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",
"ui.step_values": "Step values with the arrow keys and the wheel",
"ui.step_values_tip": "In a value field you are typing in, Up/Down change a plain number by 1 (Shift: 10, Alt: 0.1), and so does the mouse wheel over the field. Expressions are left as typed.",
"ui.stretch": "Stretch",
"ui.supersampling": "Supersampling (SSAA)",
"ui.supersampling_adaptive": "Only when the view is still",
@@ -2991,6 +3102,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",
@@ -3008,6 +3120,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",
@@ -3061,6 +3174,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",
@@ -3071,6 +3185,11 @@
"update.up_to_date": "SolidCraft is up to date.",
"update.update_now": "Update now",
"update.you_have": "You have {version}.",
"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"
}
+5 -1
View File
@@ -341,6 +341,7 @@
"Show Agent Cursor": "Mostrar cursor del agente",
"Show in folder": "Mostrar en carpeta",
"Show the pointer an AI agent moves (#35)": "Mostrar el puntero que mueve un agente de IA (#35)",
"Show tool at cursor": "Mostrar la herramienta junto al cursor",
"Signed axes": "Ejes con signo",
"Size": "Tamaño",
"Sketch constraints": "Restricciones del boceto",
@@ -2063,7 +2064,6 @@
"ui.ai_permissions": "Permisos",
"ui.ai_preset": "Preajuste",
"ui.ai_remove": "Eliminar",
"ui.ai_request_limit": "se ha detenido tras 24 solicitudes en un mensaje; envía otro para continuar",
"ui.ai_reset": "Restablecer",
"ui.ai_settings": "Ajustes de IA…",
"ui.ai_show_features": "Mostrar funciones de IA",
@@ -2240,6 +2240,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos o teclas",
"ui.findInTimeline": "Buscar en la línea de tiempo",
"ui.findInWindow": "Buscar en la ventana",
"ui.fine": "Fina",
"ui.finest": "Muy fina",
"ui.finish_sketch": "Finalizar boceto",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fijar/liberar",
@@ -2464,6 +2466,7 @@
"ui.project_face_edges_sketch": "Proyectar aristas de caras en bocetos nuevos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de tracción",
"ui.quality": "Calidad",
"ui.quantity": "Cantidad",
"ui.quantity_2": "Cantidad 2",
"ui.quit": "Salir",
@@ -2558,6 +2561,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir cuerpo",
"ui.stand_up_tip": "Tras abrir un archivo STEP o IGES con Z hacia arriba, ofrecer un cuarto de vuelta alrededor de X",
"ui.standard": "Estándar",
"ui.start": "Iniciar",
"ui.start_over": "Volver a empezar",
"ui.start_page": "Página de inicio",
+11 -1
View File
@@ -335,6 +335,7 @@
"Show Agent Cursor": "Afficher le curseur de l'agent",
"Show in folder": "Afficher dans le dossier",
"Show the pointer an AI agent moves (#35)": "Afficher le pointeur déplacé par un agent IA (#35)",
"Show tool at cursor": "Afficher l'outil près du curseur",
"Signed axes": "Axes signés",
"Size": "Taille",
"Sketch constraints": "Contraintes d'esquisse",
@@ -867,11 +868,17 @@
"help.mit": "Licence MIT",
"help.notice": "NOTICE",
"help.solid.chamfer": "Coupe les arêtes sélectionnées par un biseau plat de la distance indiquée. À utiliser pour casser des arêtes vives ou faciliter l'entrée d'une goupille ou d'une vis.",
"help.solid.combine": "Joint des corps, les retire d'un corps cible ou ne garde que le volume qu'ils ont en commun, et peut conserver les corps outils. À utiliser pour fusionner des pièces, tailler une forme avec une autre ou repérer où deux corps se recouvrent.",
"help.solid.extrude": "Pousse le profil d'une esquisse en 3D sur une distance, en ajoutant ou en retirant de la matière. À utiliser pour transformer un contour plat en bloc, en bossage ou en poche.",
"help.solid.fillet": "Arrondit les arêtes sélectionnées par un raccord circulaire lisse du rayon indiqué. À utiliser pour adoucir des arêtes vives par souci de solidité, de confort ou d'aspect.",
"help.solid.hole": "Perce un trou rond du diamètre indiqué en un point choisi d'une face. À utiliser pour les passages de vis, les goupilles et autres ouvertures rondes simples.",
"help.solid.loft": "Raccorde en douceur deux profils d'esquisse ou plus, ou un profil et un point, par un solide lissé. À utiliser pour les transitions, entonnoirs, poignées et toute forme dont la section change sur sa longueur.",
"help.solid.mirror": "Crée une copie symétrique des corps ou fonctions sélectionnés par rapport à un plan, et peut la fusionner avec l'original. À utiliser pour les pièces symétriques : on modélise une moitié et on obtient les deux.",
"help.solid.pattern.circular": "Répète les objets sélectionnés autour d'un axe, selon le nombre et l'angle total indiqués. À utiliser pour les cercles de perçages, rayons, ailettes et dents.",
"help.solid.pattern.rectangular": "Répète les objets sélectionnés dans une ou deux directions, selon le nombre et l'espacement indiqués. À utiliser pour les rangées et grilles de trous, nervures, goupilles ou pieds.",
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
"help.solid.sweep": "Entraîne le profil d'une esquisse le long d'une trajectoire pour obtenir un solide. À utiliser pour les tuyaux, rails, poignées et toute pièce de section constante qui suit une courbe.",
"help.third_party": "Licences tierces",
"home.back": "Revenir à {name}",
"home.crashes": "Rapports de plantage",
@@ -1423,7 +1430,6 @@
"ui.ai_permissions": "Autorisations",
"ui.ai_preset": "Préréglage",
"ui.ai_remove": "Retirer",
"ui.ai_request_limit": "arrêté après 24 requêtes pour un message ; envoyez un autre message pour poursuivre",
"ui.ai_reset": "Réinitialiser",
"ui.ai_settings": "Réglages IA…",
"ui.ai_show_features": "Afficher les fonctionnalités IA",
@@ -1600,6 +1606,8 @@
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
"ui.findInTimeline": "Trouver dans la chronologie",
"ui.findInWindow": "Trouver dans la fenêtre",
"ui.fine": "Fine",
"ui.finest": "Très fine",
"ui.finish_sketch": "Terminer l'esquisse",
"ui.fit": "Cadrer",
"ui.fix_unfix": "Fixer/Libérer",
@@ -1824,6 +1832,7 @@
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
"ui.properties": "Propriétés",
"ui.pull_direction": "Direction de démoulage",
"ui.quality": "Qualité",
"ui.quantity": "Quantité",
"ui.quantity_2": "Quantité 2",
"ui.quit": "Quitter",
@@ -1918,6 +1927,7 @@
"ui.spline": "Spline",
"ui.split_body": "Scinder le corps",
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
"ui.standard": "Standard",
"ui.start": "Début",
"ui.start_over": "Recommencer",
"ui.start_page": "Page d’accueil",
+5 -1
View File
@@ -335,6 +335,7 @@
"Show Agent Cursor": "Mostrar o cursor do axente",
"Show in folder": "Mostrar no cartafol",
"Show the pointer an AI agent moves (#35)": "Mostrar o punteiro que move un axente de IA (#35)",
"Show tool at cursor": "Mostrar a ferramenta xunto ao cursor",
"Signed axes": "Eixes con signo",
"Size": "Tamaño",
"Sketch constraints": "Restricións do bosquexo",
@@ -1415,7 +1416,6 @@
"ui.ai_permissions": "Permisos",
"ui.ai_preset": "Axuste predefinido",
"ui.ai_remove": "Quitar",
"ui.ai_request_limit": "detívose tras 24 solicitudes para unha mensaxe; envía outra mensaxe para continuar",
"ui.ai_reset": "Restablecer",
"ui.ai_settings": "Axustes de IA…",
"ui.ai_show_features": "Mostrar as funcións de IA",
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
"ui.findInTimeline": "Buscar na liña de tempo",
"ui.findInWindow": "Buscar na xanela",
"ui.fine": "Fina",
"ui.finest": "Moi fina",
"ui.finish_sketch": "Rematar o bosquexo",
"ui.fit": "Axustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
"ui.properties": "Propiedades",
"ui.pull_direction": "Dirección de extracción",
"ui.quality": "Calidade",
"ui.quantity": "Cantidade",
"ui.quantity_2": "Cantidade 2",
"ui.quit": "Saír",
@@ -1910,6 +1913,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir o corpo",
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
"ui.standard": "Estándar",
"ui.start": "Inicio",
"ui.start_over": "Volver comezar",
"ui.start_page": "Páxina de inicio",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Ügynök kurzorának megjelenítése",
"Show in folder": "Megjelenítés a mappában",
"Show the pointer an AI agent moves (#35)": "Az AI-ügynök által mozgatott mutató megjelenítése (#35)",
"Show tool at cursor": "Eszköz megjelenítése a kurzornál",
"Signed axes": "Előjeles tengelyek",
"Size": "Méret",
"Sketch constraints": "Vázlatkényszerek",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "Jogosultságok",
"ui.ai_preset": "Előbeállítás",
"ui.ai_remove": "Eltávolítás",
"ui.ai_request_limit": "egy üzenetre eső 24 kérés után leállt; küldjön újabb üzenetet a folytatáshoz",
"ui.ai_reset": "Visszaállítás",
"ui.ai_settings": "AI-beállítások…",
"ui.ai_show_features": "AI-funkciók megjelenítése",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
"ui.findInTimeline": "Keresés az idővonalon",
"ui.findInWindow": "Keresés az ablakban",
"ui.fine": "Finom",
"ui.finest": "Legfinomabb",
"ui.finish_sketch": "Vázlat befejezése",
"ui.fit": "Illesztés",
"ui.fix_unfix": "Rögzítés/feloldás",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
"ui.properties": "Tulajdonságok",
"ui.pull_direction": "Húzási irány",
"ui.quality": "Minőség",
"ui.quantity": "Mennyiség",
"ui.quantity_2": "2. mennyiség",
"ui.quit": "Kilépés",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Test felosztása",
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
"ui.standard": "Normál",
"ui.start": "Kezdés",
"ui.start_over": "Újrakezdés",
"ui.start_page": "Kezdőoldal",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Tampilkan Kursor Agen",
"Show in folder": "Tampilkan di folder",
"Show the pointer an AI agent moves (#35)": "Tampilkan penunjuk yang digerakkan agen AI (#35)",
"Show tool at cursor": "Tampilkan alat di dekat kursor",
"Signed axes": "Sumbu bertanda",
"Size": "Ukuran",
"Sketch constraints": "Batasan sketsa",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "Izin",
"ui.ai_preset": "Preset",
"ui.ai_remove": "Hapus",
"ui.ai_request_limit": "dihentikan setelah 24 permintaan untuk satu pesan; kirim pesan lain untuk melanjutkan",
"ui.ai_reset": "Reset",
"ui.ai_settings": "Pengaturan AI…",
"ui.ai_show_features": "Tampilkan fitur AI",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
"ui.findInTimeline": "Cari di Linimasa",
"ui.findInWindow": "Cari di Jendela",
"ui.fine": "Halus",
"ui.finest": "Sangat halus",
"ui.finish_sketch": "Selesai Sketsa",
"ui.fit": "Pas",
"ui.fix_unfix": "Kunci/Lepas Kunci",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
"ui.properties": "Properti",
"ui.pull_direction": "Arah tarik",
"ui.quality": "Kualitas",
"ui.quantity": "Jumlah",
"ui.quantity_2": "Jumlah 2",
"ui.quit": "Keluar",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Belah Bodi",
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
"ui.standard": "Standar",
"ui.start": "Mulai",
"ui.start_over": "Mulai ulang",
"ui.start_page": "Halaman awal",
+19 -1
View File
@@ -164,6 +164,7 @@
"From Bodies": "Dai corpi",
"From edges": "Spigoli di partenza",
"Full Length": "Lunghezza totale",
"Full thread depth": "Profondità filetto completa",
"Gap": "Gioco",
"Grid": "Griglia",
"HISTORY": "CRONOLOGIA",
@@ -338,6 +339,7 @@
"Show Agent Cursor": "Mostra cursore dell'agente",
"Show in folder": "Mostra nella cartella",
"Show the pointer an AI agent moves (#35)": "Mostra il puntatore mosso da un agente IA (#35)",
"Show tool at cursor": "Mostra lo strumento accanto al cursore",
"Signed axes": "Assi con segno",
"Size": "Dimensioni",
"Sketch constraints": "Vincoli dello schizzo",
@@ -381,6 +383,7 @@
"This child is shared. Deleting it removes it from every copy of its parent.": "Questo elemento figlio è condiviso. Eliminandolo verrà rimosso da ogni copia del padre.",
"This child is shared. Position changes apply to every copy of its parent.": "Questo elemento figlio è condiviso. Le modifiche alla posizione si applicano a ogni copia del padre.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Questa faccia non consente un collegamento persistente. Scegli un'altra faccia o usa esplicitamente un piano fisso.",
"Thread depth": "Profondità filetto",
"To Offset": "Fino a una distanza",
"To edges": "Spigoli di arrivo",
"Tolerance": "Tolleranza",
@@ -998,6 +1001,16 @@
"pattern.direction_1": "Direzione 1",
"pattern.direction_2": "Direzione 2",
"pattern.flip_direction": "Inverti direzione",
"perf.click_for_details": "Fai clic per i dettagli: grafico dei tempi, istogramma e ciò che la GPU ha disegnato",
"perf.draw_calls": "Chiamate di disegno",
"perf.line_segments": "Segmenti",
"perf.low_1": "1% più lento",
"perf.renderer": "Renderer",
"perf.scene_build": "Costruzione scena",
"perf.triangles": "Triangoli",
"perf.ui_time": "Tempo UI",
"perf.viewport": "Vista",
"perf.worst": "peggiore",
"pick a hole to seat it, or OK places it at the origin": "seleziona un foro in cui inserirlo, oppure premi OK per posizionarlo nell'origine",
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
"prefs.cpu": "CPU (software)",
@@ -1073,6 +1086,7 @@
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
"search.valid": "Valido",
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
"settings.frame_rate_details_note": "Aggiunge un grafico dei tempi degli ultimi fotogrammi con i triangoli disegnati sopra, un istogramma dei tempi dei fotogrammi e ciò che la GPU ha disegnato. Fai clic sulla frequenza nella vista per mostrarli o nasconderli.",
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
@@ -1418,7 +1432,6 @@
"ui.ai_permissions": "Autorizzazioni",
"ui.ai_preset": "Preimpostazione",
"ui.ai_remove": "Rimuovi",
"ui.ai_request_limit": "arrestato dopo 24 richieste per un messaggio; invia un altro messaggio per proseguire",
"ui.ai_reset": "Reimposta",
"ui.ai_settings": "Impostazioni IA…",
"ui.ai_show_features": "Mostra funzioni IA",
@@ -1595,6 +1608,8 @@
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
"ui.findInTimeline": "Trova nella sequenza temporale",
"ui.findInWindow": "Trova nella finestra",
"ui.fine": "Fine",
"ui.finest": "Finissima",
"ui.finish_sketch": "Termina schizzo",
"ui.fit": "Adatta",
"ui.fix_unfix": "Fissa/Libera",
@@ -1602,6 +1617,7 @@
"ui.flip": "Inverti",
"ui.format": "Formato",
"ui.frame_rate": "Frequenza dei fotogrammi",
"ui.frame_rate_details": "Dettagli della frequenza dei fotogrammi",
"ui.front": "Frontale",
"ui.fully_constrained": "Completamente vincolato",
"ui.function": "Funzione",
@@ -1819,6 +1835,7 @@
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
"ui.properties": "Proprietà",
"ui.pull_direction": "Direzione di estrazione",
"ui.quality": "Qualità",
"ui.quantity": "Quantità",
"ui.quantity_2": "Quantità 2",
"ui.quit": "Esci",
@@ -1913,6 +1930,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividi corpo",
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
"ui.standard": "Standard",
"ui.start": "Inizio",
"ui.start_over": "Ricomincia",
"ui.start_page": "Pagina iniziale",
+120 -1
View File
@@ -8,6 +8,7 @@
"A Single hole snaps to the middle of its face and to the centres of its round edges": "単一の穴を面の中心と丸いエッジの中心にスナップします",
"A new configuration, a copy of the active one": "アクティブな構成をコピーして新しい構成を作成",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "配置した単一の穴にドラッグ用のハンドルを付け、面をもう一度選ぶと穴を削除せずにそこへ移動します",
"A short title for the problem (optional)": "問題の短いタイトル(省略可)",
"ARRANGE": "配置",
"About SolidCraft": "SolidCraft について",
"Acceleration": "加速度",
@@ -21,6 +22,7 @@
"Add Mid Radius": "中間半径を追加",
"Add Row": "行を追加",
"Add a tool": "ツールを追加",
"Add bend": "曲げを追加",
"Add condition": "条件を追加",
"Add or edit rule": "ルールを追加または編集",
"Add to New Folder": "新しいフォルダーに追加",
@@ -35,7 +37,9 @@
"Also deletes": "同時に削除される要素:",
"Anchor": "基準点",
"Angle 2": "角度 2",
"Angle Display": "角度の表示",
"Animate": "アニメーション",
"Another bend on the far edge of the last flange": "最後のフランジの先端にさらに曲げを追加",
"App/system information and command settings are included. Review private details before sharing.": "アプリ・システム情報とコマンド設定が含まれます。共有前に非公開の情報を確認してください。",
"Appearance name": "外観名",
"Applied Radius": "適用半径",
@@ -52,6 +56,7 @@
"BOUNDARY SURFACE": "境界サーフェス",
"BROWSER": "ブラウザ",
"Bar": "平鋼",
"Bend": "曲げ角度",
"Bend Edges": "曲げエッジ",
"Bend Line Position": "曲げ線位置",
"Bend Position": "曲げ位置",
@@ -138,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)",
@@ -218,6 +224,7 @@
"Fit and G2 corners require Constant radius.": "フィットと G2 コーナーには Constant radius が必要です。",
"Fixed Face": "固定面",
"Flange thickness": "フランジ厚",
"Flanges": "フランジ",
"Flat Pattern": "フラットパターン",
"Flat size": "展開サイズ",
"Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
@@ -227,11 +234,15 @@
"From Bodies": "ボディから",
"From edges": "開始側エッジ",
"Full Length": "全長",
"Full thread depth": "ねじ深さ全体",
"Further Bends": "追加の曲げ",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 コーナーには一定半径と Exact fit が必要です。G2 コーナーの構築はまだサポートされていません。",
"Gap": "間隔",
"Grid": "グリッド",
"HISTORY": "履歴",
"Halo under sketch curves": "スケッチ曲線の下に縁取りを表示",
"Height": "高さ",
"Height From": "高さの基準",
"Hidden edges": "隠線",
"History layout": "履歴のレイアウト",
"History names": "履歴名",
@@ -247,6 +258,8 @@
"In This Design": "このデザイン内",
"In place (surfaces)": "その場で(サーフェス)",
"In plane": "平面内",
"Included": "挟角",
"Inner Sharp": "内側仮想シャープ",
"Inside": "内側",
"Instant": "即時",
"Interface font": "インターフェースフォント",
@@ -293,6 +306,7 @@
"Mass Center": "重心",
"Matched restrictions check the final values.": "条件に一致する制限ルールが最終値を確認します。",
"Material": "材料",
"Material Outside": "材料外側",
"Material copied. Existing body assignments are unchanged.": "マテリアルをコピーしました。既存のボディの割り当ては変わりません。",
"Max": "最大",
"Maximum": "最大",
@@ -332,14 +346,17 @@
"Nominal diameter": "呼び径",
"None": "なし",
"Not available yet": "まだ利用できません",
"Not saved yet": "まだ保存されていません",
"Notch": "ノッチ",
"OUTPUT": "出力",
"Of": "の値",
"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で内容を確認して投稿してください。",
@@ -347,6 +364,8 @@
"Orbit (right drag)": "回転(右ドラッグ)",
"Other side": "反対側",
"Other surface (mutual trim)": "相手のサーフェス(相互トリム)",
"Other way": "反対方向",
"Outer Sharp": "外側仮想シャープ",
"Outer radius": "外側コーナー半径",
"Outside": "外側",
"Overall length": "全長",
@@ -402,6 +421,7 @@
"Remove screenshot": "スクリーンショットを削除",
"Remove the column": "列を削除",
"Remove the track": "トラックを削除",
"Remove this bend": "この曲げを削除",
"Remove this point": "この点を削除",
"Rename Feature": "フィーチャーの名前を変更",
"Rename Folder": "フォルダーの名前を変更",
@@ -443,6 +463,7 @@
"Rules run from top to bottom. Matched drives change effective values.": "ルールは上から順に実行されます。条件に一致する駆動ルールが有効値を変更します。",
"SECTION": "断面",
"SURFACE QUALITY": "サーフェス品質",
"Same way": "同じ方向",
"Save Rule": "ルールを保存",
"Save View": "ビューを保存",
"Save screenshot…": "スクリーンショットを保存…",
@@ -471,8 +492,10 @@
"Shape": "形状",
"Sheet Rules": "板金ルール",
"Show Agent Cursor": "エージェントカーソルを表示",
"Show ghost": "ゴーストを表示",
"Show in folder": "フォルダに表示",
"Show the pointer an AI agent moves (#35)": "エージェントが操作するポインターを表示 (#35)",
"Show tool at cursor": "カーソルの横にツールを表示",
"Signed axes": "符号付き座標軸",
"Size": "サイズ",
"Sketch constraints": "スケッチ拘束",
@@ -509,9 +532,11 @@
"Surface, or faces to turn over": "サーフェス、または裏返す面",
"Swap Ends": "端点を入れ替え",
"TRIM": "トリム",
"Tangent": "正接",
"Tangent (G1)": "接線 (G1)",
"Tangent Chain": "正接チェーン",
"Tangent chain": "接線チェーン",
"Tangent to Edge": "エッジに正接",
"Tap drill diameter": "タップ下穴径",
"Target": "ターゲット",
"Target Body": "ターゲットボディ",
@@ -529,6 +554,7 @@
"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で添付してください。",
"Thread depth": "ねじ深さ",
"Thread diameter": "ねじ径",
"To": "終了",
"To Offset": "オフセットまで",
@@ -588,10 +614,13 @@
"Z angle": "Z角度",
"Z distance": "Z距離",
"Zebra": "ゼブラ",
"ai.connected_toast": "SolidCraft が {clients} で使えるようになりました。使うには再起動してください(設定 > アシスタント)。",
"ai.prefs.allow_control": "AI エージェントの接続を許可(コントロールチャンネル / MCP)",
"ai.prefs.allow_control_note": "このコンピューター内だけのコントロールチャンネル(127.0.0.1、ポート 27878 または次に空いているポート)を開きます。接続するプログラムは、起動のたびに作られ、あなたのユーザーアカウントだけが読める秘密のトークンも提示する必要があります。次に SolidCraft を起動したときから有効です。オフ:--control を付けて起動しない限り、何も待ち受けません。",
"ai.prefs.availability": "アシスタントの操作",
"ai.prefs.background_commands": "エージェントのコマンドをバックグラウンドで実行",
"ai.prefs.connect_apps": "このコンピューターの AI アプリで SolidCraft を使えるようにする",
"ai.prefs.connect_apps_note": "SolidCraft の起動時に、このコンピューターで見つかった AI アプリ(OpenCode、Claude Desktop、Claude Code、Cursor、Codex、Gemini CLI、Copilot CLI、Windsurf、Zed、VS Code)へ MCP サーバーとして追加します。追加または更新するのは solidcraft のエントリのみで、変更済みのエントリには触れず、ファイルは先にバックアップされます。オフにすると追加したエントリを削除します。",
"ai.prefs.follow_camera": "カメラをスケッチに向ける",
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
@@ -663,6 +692,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": "ボディからコンポーネントを作成",
@@ -747,6 +777,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": "曲率コーム解析",
@@ -756,6 +788,7 @@
"cmd.inspect_interference": "干渉",
"cmd.inspect_isocurve": "アイソカーブ解析",
"cmd.inspect_minimum_radius": "最小半径解析",
"cmd.inspect_parameter_sweep": "パラメータスイープ",
"cmd.inspect_silhouette": "シルエット",
"cmd.inspect_zebra": "ゼブラ解析",
"cmd.joint_as_built": "現位置ジョイント",
@@ -881,6 +914,7 @@
"cmd.sheet_fold": "折り曲げ",
"cmd.sheet_hem": "ヘミング",
"cmd.sheet_manage_rules": "板金ルール",
"cmd.sheet_path_flange": "パスフランジ",
"cmd.sheet_perforate": "パンチング",
"cmd.sheet_refold": "折り戻し",
"cmd.sheet_rules": "板金ルールを一覧表示",
@@ -1058,6 +1092,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": "計算",
@@ -1073,7 +1108,18 @@
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
"dialog.selection_step": "{total} ステップ中 {step}: {input} を選択",
"dialog.step": "ステップ",
"document.changed_on_disk": "{name} がディスク上で変更されました",
"document.changed_on_disk_detail": "{name} は、開いたとき(または最後に保存したとき)以降に別のプログラムまたはコンピューターによって変更されました。上書きするとその変更は失われます。コピーを保存すると両方が残ります。",
"document.copy_name": "{name} のコピー",
"document.open_anyway": "このまま開く",
"document.open_elsewhere": "{name} はすでに開かれています",
"document.open_elsewhere_detail": "{name} は {who} の SolidCraft {app} で開かれています({when}に開かれました)。両方で保存すると、どちらかの作業が失われます。上書きせずに確認するには読み取り専用で開いてください。",
"document.open_read_only": "読み取り専用で開く",
"document.opened_read_only": "読み取り専用で開きました:保存すると新しいファイル名を尋ねます",
"document.overwrite": "上書き",
"document.read_only_save": "このデザインは読み取り専用で開かれています:新しいファイルとして保存してください",
"document.save_changes": "{name} への変更を保存しますか?",
"document.save_copy": "コピーを保存…",
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
"drive.stopped_by_contact": "{value} で停止: {a} が {b} を通り抜けてしまいます",
"drive.stopped_by_loop": "{value} で停止: {joint} を通るループはこれ以上閉じられません",
@@ -1090,9 +1136,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": "ビューを準備中...",
@@ -1102,6 +1150,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": "形材から",
@@ -1109,17 +1158,25 @@
"help.apache": "Apacheライセンス2.0",
"help.attributions": "帰属表示 (ATTRIBUTION.md)",
"help.build": "バージョン {version} · コミット {commit} · ビルド日 {date}",
"help.connect_note": "各アシスタントを再起動すると SolidCraft サーバーが読み込まれます。変更したファイルは .bak としてバックアップされています。",
"help.connect_title": "AI アシスタントを接続",
"help.font_details": "インターフェースにはegui組み込みのフォントを使用しています(Ubuntu Font LicenceおよびMIT/Bitstream VeraライセンスのUbuntu LightとHack、OFLおよびMITのNoto Emojiと絵文字アイコンフォント)。スケッチのテキストにはLiberation Sans(SIL OFL 1.1)を使用しています。The Crafting Appsのオプションフォントパックcraft-fonts(BIZ UDPGothic、Shippori Mincho、BIZ UDMincho、Noto Sans CJK SC、Noto Sans Arabic、SIL OFL 1.1)はSolidCraftのビルドに同梱されていません。",
"help.fonts": "フォント",
"help.licences": "ライセンス",
"help.mit": "MITライセンス",
"help.notice": "NOTICE",
"help.solid.chamfer": "選択した辺を、指定した距離で平らに斜めに削ります。鋭い角を落とすときや、ピンやねじを入れやすくするときに使います。",
"help.solid.combine": "複数の立体を結合したり、対象の立体から切り取ったり、共通する体積だけを残したりでき、工具の立体を残すこともできます。部品の結合、ある形での切り抜き、2つの立体が重なる範囲の確認に使います。",
"help.solid.extrude": "スケッチの輪郭を指定した距離だけ立体方向に押し出し、材料を足したり削ったりします。平面の図形をブロック、突起、ポケットにするときに使います。",
"help.solid.fillet": "選択した辺を、指定した半径のなめらかな円弧で丸めます。強度や持ちやすさ、見た目のために鋭い角を和らげるときに使います。",
"help.solid.hole": "面上の指定した位置に、指定した直径の丸い穴をあけます。ねじのクリアランスやピン穴など、単純な丸い開口に使います。",
"help.solid.loft": "2つ以上のスケッチの輪郭の間、または輪郭から点までを、なめらかな立体でつなぎます。移行部、漏斗、ハンドルなど、長さに沿って断面が変わる形に使います。",
"help.solid.mirror": "選択した立体またはフィーチャを平面に対して鏡像でコピーし、元の立体と結合することもできます。左右対称の部品で、半分だけモデリングして両側を得るときに使います。",
"help.solid.pattern.circular": "選択したオブジェクトを、指定した数と全体の角度で軸のまわりに繰り返し配置します。ボルト円、スポーク、フィン、歯などに使います。",
"help.solid.pattern.rectangular": "選択したオブジェクトを、指定した数と間隔で1方向または2方向に繰り返し配置します。穴、リブ、ピン、脚の列や格子状の配置に使います。",
"help.solid.revolve": "スケッチの輪郭を軸のまわりに回転させ、丸い立体を作ります。軸、つまみ、ボウルなど、中心線まわりに丸い部品に使います。",
"help.solid.shell": "立体の中をくり抜き、指定した厚さの壁を残します。選んだ面を開口にすることもできます。箱、カバーなどの薄肉の部品に使います。",
"help.solid.sweep": "スケッチの輪郭をパスに沿って動かし、立体を作ります。パイプ、レール、ハンドルなど、一定の断面で曲線をたどる部品に使います。",
"help.third_party": "サードパーティライセンス",
"home.back": "{name} に戻る",
"home.crashes": "クラッシュレポート",
@@ -1196,6 +1253,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}をそのまま使用",
@@ -1301,10 +1360,22 @@
"pattern.direction_1": "方向 1",
"pattern.direction_2": "方向 2",
"pattern.flip_direction": "方向を反転",
"perf.click_for_details": "クリックで詳細: フレーム時間のグラフ、ヒストグラム、GPU の描画内容",
"perf.draw_calls": "描画コール",
"perf.line_segments": "線分",
"perf.low_1": "下位 1%",
"perf.renderer": "レンダラー",
"perf.scene_build": "シーン構築",
"perf.triangles": "三角形",
"perf.ui_time": "UI 時間",
"perf.viewport": "ビュー",
"perf.worst": "最悪",
"pick a hole to seat it, or OK places it at the origin": "配置先の穴を選択するか、[OK]を押すと原点に配置します。",
"pick one or two vertices, edges or faces": "1つまたは2つの頂点、エッジ、面を選択",
"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": "アシスタント",
@@ -1348,6 +1419,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": "履歴",
@@ -1379,6 +1454,7 @@
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
"prefs.orientation": "方向",
"prefs.overall_speed": "全体の速度",
"prefs.prefer_free_sketch_points": "スケッチ点が重なったときは移動可能な点を優先する",
"prefs.preview_choices": "選択した設定のプレビュー",
"prefs.quality_default": "既定",
"prefs.quality_fastest": "最速",
@@ -2052,7 +2128,9 @@
"search.try": "コマンド名、このデザインのフィーチャー、またはパラメーターを入力します。値を指定して実行するには、IDとJSONオブジェクトを入力します:{example}",
"search.valid": "有効",
"section.off_hint": "平面が選択されていません。OK を押すとセクションを解除します。",
"set by the sketch's lines": "スケッチの線で決まります",
"settings.display_quality_note": "表示だけに影響します。設計や書き出しは変わりません。",
"settings.frame_rate_details_note": "直近のフレーム時間のグラフ(描画した三角形数を重ねて表示)、フレーム時間のヒストグラム、GPU が描画した内容を追加します。ビューのフレームレートをクリックすると表示/非表示を切り替えます。",
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。",
"settings.measure_axis_distances": "計測のX・Y・Z距離を表示",
"settings.measure_axis_distances_help": "計測ダイアログを開いている間、2点間距離に加えてワールドX・Y・Z軸方向の寸法線と距離を表示します。",
@@ -2083,6 +2161,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 を押してください。",
@@ -2107,6 +2190,7 @@
"timeline.delete_after_marker_one": "履歴マーカー以降の 1 個のフィーチャーを削除",
"timeline.delete_after_marker_other": "履歴マーカー以降の {count} 個のフィーチャーを削除",
"timeline.delete_many": "選択した {count} 個のフィーチャーを削除",
"timeline.feature_hint": "ダブルクリックで編集 · ドラッグで並べ替え · 右クリックでその他の操作",
"timeline.group": "フィーチャーフォルダを新規作成",
"timeline.move_many": "タイムライン項目を移動",
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
@@ -2254,7 +2338,9 @@
"ui.agent_chat_auth_title": "エージェントにサインイン",
"ui.agent_chat_cancel": "キャンセル",
"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": "接続",
@@ -2305,6 +2391,7 @@
"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": "新しいチャットを始めますか? 現在のチャットは消去されます。",
@@ -2313,6 +2400,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": "未インストール",
@@ -2330,6 +2418,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": "待機中",
@@ -2358,6 +2449,7 @@
"ui.agent_chat_stop_tip": "エージェントと、それが起動したものをすべて停止します",
"ui.agent_chat_stop_turn_tip": "エージェントを停止します(Esc)。待機中のメッセージは待機のままです。",
"ui.agent_chat_turn_limit": "1メッセージあたりの変更数",
"ui.agent_chat_turn_unlimited": "無制限",
"ui.agent_chat_undo_changes": "これらの変更を元に戻す",
"ui.agent_chat_undo_label": "エージェントチャット: {message}",
"ui.agent_chat_undone": "(元に戻しました)",
@@ -2426,7 +2518,9 @@
"ui.ai_preset": "プリセット",
"ui.ai_protocol": "プロトコル",
"ui.ai_remove": "削除",
"ui.ai_request_limit": "1メッセージあたり24リクエストで停止しました。続けるには、もう一度メッセージを送ってください",
"ui.ai_request_timed_out": "モデルの応答に {seconds} 秒以上かかりました。このエンドポイントのリクエストのタイムアウトは、環境設定 › アシスタントで長くできます。",
"ui.ai_request_timeout": "リクエストのタイムアウト",
"ui.ai_request_timeout_hint": "このエンドポイントへのモデルリクエスト1回にかけられる時間です(30秒〜1時間)。遅いローカルモデルでは長くしてください。",
"ui.ai_reset": "リセット",
"ui.ai_settings": "AI 設定…",
"ui.ai_show_features": "AI 機能を表示",
@@ -2534,6 +2628,8 @@
"ui.copy": "コピー",
"ui.corner": "コーナー",
"ui.corners": "コーナー",
"ui.cosmetic_thread_markings": "ねじの簡略表示",
"ui.cosmetic_thread_markings_help": "ねじ付き円筒面に簡略化したマークを表示します。表示のみを変更し、設計やエクスポートには影響しません。",
"ui.counterbore": "座ぐり",
"ui.countersink": "皿穴",
"ui.create": "作成",
@@ -2610,6 +2706,8 @@
"ui.filter_commands_or_keys": "コマンドまたはキーを検索",
"ui.findInTimeline": "タイムラインで検索",
"ui.findInWindow": "ウィンドウで検索",
"ui.fine": "細かい",
"ui.finest": "最も細かい",
"ui.finish_sketch": "スケッチを終了",
"ui.fit": "フィット",
"ui.fix_unfix": "固定/固定解除",
@@ -2617,6 +2715,7 @@
"ui.flip": "反転",
"ui.format": "形式",
"ui.frame_rate": "フレームレート",
"ui.frame_rate_details": "フレームレートの詳細",
"ui.front": "正面",
"ui.fully_constrained": "完全拘束",
"ui.function": "関数",
@@ -2802,6 +2901,7 @@
"ui.one_side": "片側",
"ui.open": "開く",
"ui.open_ends_tip": "どこにも接していない",
"ui.open_linked_design": "リンクされたデザインを開く",
"ui.open_preferences": "環境設定を開く",
"ui.open_recent": "最近使った項目を開く",
"ui.operation": "操作",
@@ -2821,6 +2921,11 @@
"ui.paste": "貼り付け",
"ui.path": "パス",
"ui.pattern_on_path": "パス上のパターン",
"ui.perforate_margin_default": "直径と同じ",
"ui.perforate_pitch": "ピッチ",
"ui.perforate_square": "正方配列",
"ui.perforate_staggered": "千鳥配列(60°)",
"ui.perforate_title": "パーフォレーション",
"ui.perpendicular": "直交",
"ui.perspective_view": "パースペクティブ表示",
"ui.pinToShortcuts": "ショートカットに固定",
@@ -2832,7 +2937,9 @@
"ui.plugins": "プラグイン",
"ui.plugins_desktop": "プラグインはデスクトップ版で使えます。",
"ui.plugins_empty": "プラグインは入っていません。",
"ui.plugins_none": "検索に一致するプラグインはありません。",
"ui.plugins_note": "プラグインは {path} の中のフォルダで、plugin.json を置きます。SolidCraft はそのファイルを読むだけで、フォルダのコードは実行しません。オフにしてもインストールは残ります。",
"ui.plugins_search": "プラグインを検索",
"ui.plugins_skipped": "{count} 個のフォルダを読めませんでした。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2848,6 +2955,7 @@
"ui.project_face_edges_sketch": "新規スケッチに面のエッジを自動投影",
"ui.properties": "プロパティ",
"ui.pull_direction": "抜き方向",
"ui.quality": "品質",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "終了",
@@ -2954,9 +3062,12 @@
"ui.spline": "スプライン",
"ui.split_body": "ボディを分割",
"ui.stand_up_tip": "Z軸上向きで開いたSTEPまたはIGESファイルに対し、X軸周りに90度回転させるか確認します",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "最初からやり直す",
"ui.start_page": "スタートページ",
"ui.step_values": "矢印キーとホイールで数値を増減",
"ui.step_values_tip": "入力中の値フィールドでは、上下矢印キーとフィールド上のマウスホイールで数値を 1 ずつ増減します(Shift: 10、Alt: 0.1)。式はそのまま残ります。",
"ui.stretch": "伸縮",
"ui.supersampling": "スーパーサンプリング (SSAA)",
"ui.supersampling_adaptive": "ビューが静止しているときのみ",
@@ -2991,6 +3102,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": "両側",
@@ -3008,6 +3120,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": "表示スタイル",
@@ -3061,6 +3174,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": "後で",
@@ -3071,6 +3185,11 @@
"update.up_to_date": "SolidCraft は最新です。",
"update.update_now": "今すぐ更新",
"update.you_have": "現在のバージョンは {version} です。",
"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 閉じる"
}
+306 -1
View File
@@ -14,11 +14,15 @@
"Active": "활성",
"Active rule": "활성 규칙",
"Add": "추가",
"Add Column": "열 추가",
"Add Key": "키 추가",
"Add Mid Radius": "중간 반지름 추가",
"Add Row": "행 추가",
"Add a tool": "도구 추가",
"Add condition": "조건 추가",
"Add or edit rule": "규칙 추가 또는 편집",
"Add to New Folder": "새 폴더에 추가",
"Advanced": "고급",
"Against": "상대",
"Alignment": "정렬",
"All Sketch Points": "모든 스케치 점",
@@ -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 or JSON: updates existing parameters, adds new ones",
"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.": "유형에서 가변 반지름 또는 스플라인을 선택하세요.",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "새 평면을 선택하세요. 스케치 기하와 치수는 2차원 좌표 그대로 유지됩니다.",
"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": "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": "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": "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",
"Filter…": "필터…",
"Find in Browser": "브라우저에서 찾기",
"Finish": "완료",
"First plane": "첫 번째 평면",
"Fit and G2 corners require Constant radius.": "맞춤 및 G2 모서리에는 일정 반지름이 필요합니다.",
"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 모서리에는 일정 반지름과 정확한 맞춤이 필요합니다. 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,14 +301,19 @@
"No matching appearance presets.": "일치하는 외관 프리셋이 없습니다.",
"No matching appearances": "일치하는 외관 없음",
"No visible bodies or sketches to export.": "표시된 바디나 스케치가 없어 내보낼 수 없습니다.",
"Nominal diameter": "호칭 지름",
"None": "없음",
"Not available yet": "아직 사용할 수 없음",
"Notch": "노치",
"OUTPUT": "출력",
"On your computer": "내 컴퓨터",
"Opacity": "불투명도",
"Open GitHub draft": "GitHub 초안 열기",
"Open library...": "라이브러리 열기...",
"Open local libraries in the desktop app. You can export this design's library here.": "로컬 라이브러리는 데스크톱 앱에서 여세요. 이 디자인의 라이브러리는 여기서 내보낼 수 있습니다.",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust로 만든 오픈 소스 매개변수 3D CAD. 라이선스: MIT OR Apache-2.0.",
"Opens your browser and copies the report text for you to paste into GitHub.": "브라우저를 열고, GitHub에 붙여넣을 수 있도록 보고서 텍스트를 복사합니다.",
"Opens your browser. Review and submit your report on GitHub.": "브라우저를 엽니다. GitHub에서 보고서를 검토하고 제출하세요.",
"Operator": "연산자",
"Orbit (right drag)": "회전 (오른쪽 드래그)",
"Other side": "반대쪽",
@@ -259,8 +330,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": "위치",
@@ -268,14 +342,18 @@
"Position (G0)": "위치 (G0)",
"Position along the edge, 0 at the start end, 1 at the other": "모서리를 따른 위치, 0은 시작점, 1은 반대쪽 끝",
"Preferences › Design turns the offer back on": "환경 설정 › 디자인에서 다시 켤 수 있습니다",
"Preview": "미리보기",
"Preview Row": "행 미리보기",
"Preview leaves the current design unchanged.": "미리보기는 현재 디자인을 변경하지 않습니다.",
"Profile / Edges": "프로파일 / 모서리",
"Profiles or curves": "프로파일 또는 곡선",
"Profiles/Curves": "프로파일/곡선",
"Quantity": "양",
"RECENT": "최근",
"REFERENCE": "참조",
"RULED SURFACE": "룰드 서피스",
"RULES": "규칙",
"Radius": "반지름",
"Radius Fit": "반지름 맞춤",
"Ratio": "비율",
"Reattach selected face": "선택한 면 다시 부착",
@@ -284,6 +362,8 @@
"Rectangular": "직사각형",
"Reference face": "참조 면",
"Remove Tools": "공구 제거",
"Remove condition": "조건 제거",
"Remove from Folder": "폴더에서 제거",
"Remove from list": "목록에서 제거",
"Remove override": "재정의 제거",
"Remove private design details or paths before sharing.": "공유하기 전에 비공개 디자인 정보나 경로를 제거하세요.",
@@ -291,12 +371,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": "반전",
@@ -306,7 +392,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": "Rule",
@@ -316,26 +409,34 @@
"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": "에이전트 커서 표시",
"Show in folder": "폴더에서 보기",
"Show the pointer an AI agent moves (#35)": "AI 에이전트가 움직이는 포인터 표시 (#35)",
"Show tool at cursor": "커서 옆에 도구 표시",
"Signed axes": "부호 있는 축",
"Size": "크기",
"Sketch constraints": "스케치 구속 조건",
@@ -348,6 +449,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축 기준 90도 회전으로, 실행 취소할 수 있는 하나의 이동 피처로 적용됩니다.",
@@ -357,17 +459,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": "Then",
@@ -379,24 +491,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": "Value",
"Variable Radius": "가변 반지름",
"Velocity": "속도",
"Vertices": "꼭짓점",
"View navigator": "뷰 탐색기",
@@ -404,16 +524,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": "When",
"Width": "너비",
"Will fail": "실패 예정",
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "제스처를 켜면 오른쪽 버튼으로 더 이상 뷰가 회전하지 않습니다. 버튼을 누른 채 움직여 링을 열고 도구 영역에 닿으세요. 그냥 오른쪽 클릭하면 여전히 메뉴가 열립니다.",
"X": "X",
"X Angle": "X 각도",
"X angle": "X 각도",
"X distance": "X 거리",
"Y": "Y",
"Y Angle": "Y 각도",
"Y angle": "Y 각도",
"Y distance": "Y 거리",
"Z": "Z",
"Z Angle": "Z 각도",
"Z Distance": "Z 거리",
"Z angle": "Z 각도",
"Z distance": "Z 거리",
"Zebra": "제브라",
"ai.prefs.availability": "어시스턴트 제어",
"ai.prefs.background_commands": "에이전트 명령을 백그라운드에서 실행",
"ai.prefs.follow_camera": "카메라를 스케치로 돌리기",
"ai.prefs.follow_camera_off": "카메라 자동 따라가기가 꺼져 있습니다.",
"ai.prefs.follow_camera_on": "에이전트가 만드는 스케치를 향해 카메라가 회전합니다.",
"ai.prefs.intro": "에이전트, 권한, 그리고 작업이 표시되는 방식.",
"ai.prefs.make_default": "기본값으로 사용",
"ai.prefs.no_agents": "아직 에이전트가 없습니다. 에이전트를 추가하거나 새로 고쳐서 설치된 에이전트를 찾으세요.",
"ai.prefs.no_saved_approvals": "저장된 승인이 없습니다.",
"ai.prefs.override_all_warning": "이 에이전트는 묻지 않고 설계를 저장, 덮어쓰기, 내보내기 또는 교체할 수 있습니다.",
"ai.prefs.permission_all": "모두 허용",
"ai.prefs.permission_all_summary": "사용 가능한 SolidCraft 도구가 묻지 않고 실행됩니다.",
"ai.prefs.permission_ask": "변경 전에 묻기",
"ai.prefs.permission_ask_summary": "모든 설계 변경과 파일 작업을 승인합니다.",
"ai.prefs.permission_edits": "모델 편집 허용",
"ai.prefs.permission_edits_summary": "모델 편집은 실행되고, 파일 및 파괴적인 작업은 계속 묻습니다.",
"ai.prefs.permission_scope": "SolidCraft 도구에만 적용됩니다. 에이전트의 셸과 파일 편집은 자체 권한을 사용합니다.",
"ai.prefs.pointer_preview_hidden": "포인터 숨김",
"ai.prefs.pointer_preview_speed": "에이전트 포인터 · {speed}",
"ai.prefs.pointer_speed": "동작 속도",
"ai.prefs.privacy_note": "선택한 에이전트 또는 엔드포인트만 대화를 받습니다. 엔드포인트 키는 시스템 키체인에 저장됩니다.",
"ai.prefs.show_pointer": "에이전트 포인터 표시",
"ai.prefs.turn_limit_hint": "메시지 하나에 어시스턴트가 수행할 수 있는 설계 변경 도구 호출의 최대 횟수입니다.",
"angle, height": "각도, 높이",
"appearance.dielectric_ior": "유전체 굴절률",
"appearance.metallic": "금속성",
@@ -887,6 +1041,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": "충돌 보고서",
@@ -996,8 +1156,15 @@
"motion.to_end": "끝으로",
"motion.to_start": "처음으로",
"motion.track_name": "트랙 {number}",
"nav.alias.help": "Shift+Alt+왼쪽 드래그는 회전, Shift+Alt+가운데 드래그는 이동, Shift+Alt+오른쪽 드래그는 확대/축소입니다. 가운데 드래그로도 이동하고 오른쪽 드래그로도 회전합니다",
"nav.blender.help": "가운데 드래그는 회전, Shift+가운데 드래그는 이동, Ctrl+가운데 드래그는 확대/축소입니다. 오른쪽 드래그는 카메라에 영향을 주지 않습니다",
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
"nav.free_orbit.label": "자유 회전",
"nav.fusion.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, 오른쪽 드래그는 회전입니다",
"nav.inventor.help": "가운데 드래그는 이동, Shift+가운데 드래그는 회전, Ctrl+가운데 드래그는 확대/축소, 오른쪽 드래그는 회전입니다",
"nav.openscad.help": "빈 공간에서 왼쪽 드래그는 회전, Shift+왼쪽 드래그는 상자 선택, 스케치 형상 드래그는 편집입니다. 오른쪽 또는 가운데 드래그는 이동, Shift+오른쪽 또는 Shift+가운데 드래그는 확대/축소입니다",
"nav.solidworks.help": "가운데 드래그는 회전, Ctrl+가운데 드래그는 이동, Shift+가운데 드래그는 확대/축소, 오른쪽 드래그는 회전입니다",
"nav.tinkercad.help": "오른쪽 드래그는 회전, 가운데 또는 Shift+오른쪽 드래그는 이동입니다",
"nav.touchpad.help": "Shift를 누른 채 포인터를 움직이면 화면이 이동하고, Alt/Option을 누른 채 움직이면 회전합니다. 마우스 버튼은 필요 없습니다. 세로 스크롤로 확대/축소합니다.",
"nav.touchpad.label": "터치패드",
"nav.touchpad.orbit": "회전 (Alt/Option을 누르고 이동, 마우스 버튼 불필요)",
@@ -1020,8 +1187,130 @@
"pattern.flip_direction": "방향 반전",
"pick a hole to seat it, or OK places it at the origin": "안착시킬 구멍을 선택하거나, 확인을 누르면 원점에 배치됩니다",
"pick one or two vertices, edges or faces": "꼭짓점, 모서리 또는 면을 하나나 두 개 선택하세요",
"prefs.add_item": "항목 추가",
"prefs.alt_frees_manipulators": "Alt / Option 키로 조작 화살표도 자유롭게 움직입니다",
"prefs.anti_aliasing_note": "울퉁불퉁한 모서리를 부드럽게 합니다.",
"prefs.applies_on_restart": "다시 시작하면 적용됩니다",
"prefs.assistant": "어시스턴트",
"prefs.axes": "축",
"prefs.axis": "축",
"prefs.bottom": "아래쪽",
"prefs.browser": "브라우저",
"prefs.built_in": "기본 제공",
"prefs.button_left": "왼쪽 드래그",
"prefs.button_middle": "가운데 드래그",
"prefs.button_right": "오른쪽 드래그",
"prefs.choose_font": "글꼴 파일 선택…",
"prefs.click": "클릭",
"prefs.contact_shading": "부드러운 접촉 음영",
"prefs.contact_shading_note": "부품이 가까워지는 곳의 부드러운 음영.",
"prefs.context_menu": "컨텍스트 메뉴",
"prefs.context_menu_ring": "컨텍스트 메뉴 · 길게 누르면 방사형 메뉴",
"prefs.cpu": "CPU (소프트웨어)",
"prefs.crash_reports": "충돌 보고서 열기…",
"prefs.crash_reports_note": "보고서는 문제를 재현하는 설계 및 명령과 함께 이 컴퓨터에 보관됩니다.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "곡면",
"prefs.curves": "곡면",
"prefs.dead_zone_note": "빗금 친 영역이 데드존입니다. 그 안의 움직임은 무시됩니다.",
"prefs.default": "기본값",
"prefs.design": "설계 기본값",
"prefs.design_note": "새 설계가 시작할 때의 설정입니다. 열려 있는 설계는 자체 설정을 유지합니다.",
"prefs.devices": "3D 마우스",
"prefs.display": "표시",
"prefs.edges": "모서리 및 음영",
"prefs.extend": "확장",
"prefs.extra_navigation": "Ctrl+왼쪽 드래그로 빈 공간에서 회전하고, Space를 누른 채 이동합니다.",
"prefs.follows_os": "운영 체제 설정을 따름",
"prefs.font": "글꼴",
"prefs.font_fallback": "선택한 글꼴에 없는 글리프는 기본 제공 글꼴로 대체되므로 빈 상자로 표시되는 레이블이 없습니다.",
"prefs.font_sample": "The quick brown fox · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "선택한 글꼴을 읽을 수 없습니다.",
"prefs.general": "일반",
"prefs.general_note": "언어, 저장 및 업데이트.",
"prefs.gesture_orbit": "회전",
"prefs.gesture_pan": "이동",
"prefs.gesture_zoom": "확대/축소",
"prefs.graphics": "그래픽",
"prefs.graphics_note": "속도와 부드러움 중에서 선택합니다. 각 선택의 효과가 샘플에 표시됩니다.",
"prefs.graphics_presets_note": "프리셋은 아래의 선택 항목을 한 번에 설정합니다.",
"prefs.history": "이력",
"prefs.hold_stop_snapping": "누르고 있으면 스냅 중지",
"prefs.icons_only": "아이콘만",
"prefs.imported_file": "가져온 파일",
"prefs.input": "입력",
"prefs.interface": "인터페이스",
"prefs.interface_language": "인터페이스 언어",
"prefs.interface_note": "SolidCraft의 모양과 각 부분의 위치입니다.",
"prefs.interval_minutes": "{n}분",
"prefs.labels": "레이블",
"prefs.layout": "레이아웃",
"prefs.layout_note": "컴팩트는 패널을 창 가장자리에 고정합니다. 브라우저 반투명은 플로팅에서 사용할 수 있습니다. 어시스턴트 위치는 어시스턴트 페이지에서 바꿀 수 있습니다.",
"prefs.left": "왼쪽",
"prefs.left_click": "왼쪽 클릭",
"prefs.live_input": "실시간 입력",
"prefs.live_preview": "실시간 미리보기",
"prefs.minutes": "분",
"prefs.minutes_suffix": " min",
"prefs.mouse": "마우스 및 트랙패드",
"prefs.mouse_heading": "마우스",
"prefs.mouse_mapping": "마우스 매핑",
"prefs.navigation": "탐색",
"prefs.navigation_note": "마우스, 트랙패드, 3D 마우스로 뷰를 움직이는 방식입니다.",
"prefs.not_on_gpu": "이 GPU에서는 사용할 수 없음",
"prefs.on": "켜짐",
"prefs.opaque": "불투명",
"prefs.orbit_snap": "회전하는 동안 Shift를 누르면 축에 맞춰집니다.",
"prefs.orientation": "방향",
"prefs.overall_speed": "전체 속도",
"prefs.preview_choices": "선택 항목 미리보기",
"prefs.quality_default": "기본값",
"prefs.quality_fastest": "가장 빠름",
"prefs.quality_slowest": "가장 느림",
"prefs.quality_smoother": "더 부드럽게",
"prefs.reports_saved": "{n}개 저장됨",
"prefs.right_click": "오른쪽 클릭",
"prefs.saving": "자동 저장 및 복구",
"prefs.scale_note": "창에서 Ctrl + / Ctrl −로도 조절합니다. 버튼은 100%에서 28 px입니다.",
"prefs.scale_text": "배율 및 텍스트",
"prefs.select": "선택",
"prefs.select_one_item": "항목 하나 선택",
"prefs.selecting": "선택",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift는 추가만, Ctrl / Cmd는 토글",
"prefs.shift_add_note": "켬: Shift는 추가, Ctrl / Cmd는 토글. 끔(기본값): 둘 다 토글.",
"prefs.shortcuts": "단축키",
"prefs.sketch_selection": "스케치 및 선택",
"prefs.sketch_selection_heading": "스케치 및 선택",
"prefs.sketch_selection_note": "스케치가 스냅되는 방식과 클릭으로 선택하는 방식입니다.",
"prefs.sketching": "스케치",
"prefs.snap_drawing_note": "스케치 점을 그리거나 드래그하는 동안.",
"prefs.source": "소스",
"prefs.speed": "속도",
"prefs.stand_up_note": "STEP 또는 IGES 파일이 Z가 위쪽인 상태로 열린 후 X축 기준 90도 회전을 제안합니다.",
"prefs.status_done": "완료",
"prefs.status_error": "오류",
"prefs.status_selected": "선택됨",
"prefs.status_warning": "경고",
"prefs.supersampling_brief": "뷰를 더 크게 그린 뒤 줄여서 필터링합니다. 매우 선명하지만 더 무겁습니다.",
"prefs.tab_accent_note": "강조 색상이 활성 탭을 따라갑니다",
"prefs.theme_file": "테마 파일",
"prefs.timeline_half_hour": "−30분",
"prefs.timeline_now": "지금",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "항목을 추가하거나 다시 제거",
"prefs.toolbar": "도구 모음",
"prefs.translucent": "반투명",
"prefs.under_browser": "브라우저 아래",
"prefs.under_cursor": "커서 아래",
"prefs.units_precision": "단위 및 정밀도",
"prefs.updates": "업데이트",
"prefs.view": "뷰 동작",
"prefs.viewport": "뷰포트",
"prefs.wheel": "휠",
"prefs.wheel_trackpad": "휠 및 트랙패드",
"prefs.workspace": "작업 공간",
"prefs.zoom_toward_cursor": "확대/축소, 커서 방향으로",
"properties.title": "속성: {name}",
"recovery.days_ago": "{count}일 전",
"recovery.detail": "{file} · 피처 {features}개 · {when} 자동 저장",
@@ -1241,6 +1530,7 @@
"thread.designation": "호칭",
"thread.iso_metric": "ISO 미터",
"thread.select_size": "크기 선택",
"timeline.feature_hint": "두 번 클릭하여 편집 · 드래그하여 순서 변경 · 마우스 오른쪽 버튼을 클릭하여 더 보기",
"timeline.group": "새 특성 폴더",
"timeline.move_many": "타임라인 항목 이동",
"timeline.move_to_group": "특성을 폴더로 이동",
@@ -1490,6 +1780,8 @@
"ui.agent_chat_waiting": "확인 대기 중",
"ui.agent_cursor": "에이전트 커서",
"ui.agent_cursor_speed": "에이전트 커서 속도",
"ui.agent_off_thread": "에이전트가 명령을 실행하는 동안 창 반응성 유지",
"ui.agent_off_thread_note": "에이전트의 느린 명령(AI 패널 또는 MCP 클라이언트에서 실행)은 창과 별도로 계산됩니다. 뷰를 계속 회전하며 살펴볼 수 있고, 뷰의 칩에서 하나씩 중지할 수 있습니다. 끄면 각 명령이 창의 스레드에서 실행되며, 창은 명령이 끝날 때까지 기다립니다.",
"ui.ai_add": "추가",
"ui.ai_add_agent": "에이전트 추가…",
"ui.ai_add_agent_title": "에이전트 추가",
@@ -1545,7 +1837,6 @@
"ui.ai_permissions": "권한",
"ui.ai_preset": "프리셋",
"ui.ai_remove": "제거",
"ui.ai_request_limit": "메시지 하나에 24번 요청 후 멈췄습니다. 계속하려면 메시지를 하나 더 보내세요",
"ui.ai_reset": "재설정",
"ui.ai_settings": "AI 설정…",
"ui.ai_show_features": "AI 기능 표시",
@@ -1722,6 +2013,8 @@
"ui.filter_commands_or_keys": "명령 또는 키 필터",
"ui.findInTimeline": "타임라인에서 찾기",
"ui.findInWindow": "창에서 찾기",
"ui.fine": "세밀",
"ui.finest": "가장 세밀",
"ui.finish_sketch": "스케치 종료",
"ui.fit": "맞춤",
"ui.fix_unfix": "고정/고정 해제",
@@ -1819,10 +2112,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": "구멍 유형",
@@ -1900,6 +2200,7 @@
"ui.one_side": "한 방향",
"ui.open": "열기",
"ui.open_ends_tip": "아무것과도 닿지 않음",
"ui.open_linked_design": "연결된 디자인 열기",
"ui.open_preferences": "환경 설정 열기",
"ui.open_recent": "최근 항목 열기",
"ui.operation": "작업",
@@ -1946,6 +2247,7 @@
"ui.project_face_edges_sketch": "면 모서리를 새 스케치로 투영",
"ui.properties": "속성",
"ui.pull_direction": "빼기 방향",
"ui.quality": "품질",
"ui.quantity": "수량",
"ui.quantity_2": "수량 2",
"ui.quit": "종료",
@@ -2046,6 +2348,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": "시작 페이지",
@@ -2099,6 +2402,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": "시각 스타일",
@@ -2148,6 +2452,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": "나중에",
+5 -1
View File
@@ -335,6 +335,7 @@
"Show Agent Cursor": "Papar Kursor Ejen",
"Show in folder": "Papar dalam folder",
"Show the pointer an AI agent moves (#35)": "Papar penuding yang digerakkan oleh ejen AI (#35)",
"Show tool at cursor": "Tunjukkan alat di sebelah kursor",
"Signed axes": "Paksi bertanda",
"Size": "Saiz",
"Sketch constraints": "Kekangan lakaran",
@@ -1415,7 +1416,6 @@
"ui.ai_permissions": "Kebenaran",
"ui.ai_preset": "Pratetap",
"ui.ai_remove": "Buang",
"ui.ai_request_limit": "berhenti selepas 24 permintaan untuk satu mesej; hantar mesej lain untuk meneruskan",
"ui.ai_reset": "Set Semula",
"ui.ai_settings": "Tetapan AI…",
"ui.ai_show_features": "Papar ciri AI",
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
"ui.findInTimeline": "Cari dalam Garis Masa",
"ui.findInWindow": "Cari dalam Tetingkap",
"ui.fine": "Halus",
"ui.finest": "Paling halus",
"ui.finish_sketch": "Tamatkan Lakaran",
"ui.fit": "Muatkan",
"ui.fix_unfix": "Tetap/Lepas",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
"ui.properties": "Sifat",
"ui.pull_direction": "Arah tarikan",
"ui.quality": "Kualiti",
"ui.quantity": "Kuantiti",
"ui.quantity_2": "Kuantiti 2",
"ui.quit": "Keluar",
@@ -1910,6 +1913,7 @@
"ui.spline": "Splin",
"ui.split_body": "Belah Jasad",
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
"ui.standard": "Standard",
"ui.start": "Mula",
"ui.start_over": "Mula semula",
"ui.start_page": "Halaman mula",
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Agentcursor tonen",
"Show in folder": "Tonen in map",
"Show the pointer an AI agent moves (#35)": "De aanwijzer tonen die een AI-agent verplaatst (#35)",
"Show tool at cursor": "Gereedschap bij de cursor tonen",
"Signed axes": "Assen met teken",
"Size": "Grootte",
"Sketch constraints": "Schetsbeperkingen",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "Machtigingen",
"ui.ai_preset": "Voorinstelling",
"ui.ai_remove": "Verwijderen",
"ui.ai_request_limit": "gestopt na 24 verzoeken voor één bericht; stuur nog een bericht om verder te gaan",
"ui.ai_reset": "Herstellen",
"ui.ai_settings": "AI-instellingen…",
"ui.ai_show_features": "AI-functies tonen",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
"ui.findInTimeline": "Zoeken in tijdlijn",
"ui.findInWindow": "Zoeken in venster",
"ui.fine": "Fijn",
"ui.finest": "Zeer fijn",
"ui.finish_sketch": "Schets voltooien",
"ui.fit": "Passend",
"ui.fix_unfix": "Vastzetten/losmaken",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
"ui.properties": "Eigenschappen",
"ui.pull_direction": "Trekrichting",
"ui.quality": "Kwaliteit",
"ui.quantity": "Aantal",
"ui.quantity_2": "Aantal 2",
"ui.quit": "Afsluiten",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Lichaam splitsen",
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
"ui.standard": "Standaard",
"ui.start": "Start",
"ui.start_over": "Opnieuw beginnen",
"ui.start_page": "Startpagina",
+24
View File
@@ -0,0 +1,24 @@
# Pending UI strings
A pull request that adds UI strings adds **one new file here** instead of editing
`assets/locales/<locale>.json`. Two pull requests then never touch the same file, so they never
conflict.
- Name it after the change, lowercase with dashes: `sketch-trim-hints.json`.
- Contents: a map from locale code to `{"message id": "text"}`. Give English; add other locales
you can translate (a locale you leave out falls back to English, as before):
```json
{
"en": { "sketch.trim.hint": "Click a segment to trim it" },
"de": { "sketch.trim.hint": "Segment zum Kürzen anklicken" }
}
```
- The id must not already be in that locale's catalog, nor in another pending file; the build and
`cargo xtask lists` fail otherwise. To change an existing message, edit the catalog itself.
- Don't sort or fold anything. The app, `cargo xtask i18n` and the tests read these files as part of
the catalogs, so your strings work in your own pull request.
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it adds the
messages to the catalogs (sorted, one per line) and deletes the files. Never run it in a pull
request.
+5 -1
View File
@@ -352,6 +352,7 @@
"Show Agent Cursor": "Pokaż kursor agenta",
"Show in folder": "Pokaż w folderze",
"Show the pointer an AI agent moves (#35)": "Pokaż wskaźnik poruszany przez agenta AI (#35)",
"Show tool at cursor": "Pokaż narzędzie przy kursorze",
"Signed axes": "Osie ze znakiem",
"Size": "Rozmiar",
"Sketch constraints": "Wiązania szkicu",
@@ -1601,7 +1602,6 @@
"ui.ai_permissions": "Uprawnienia",
"ui.ai_preset": "Ustawienie wstępne",
"ui.ai_remove": "Usuń",
"ui.ai_request_limit": "zatrzymano po 24 żądaniach dla jednej wiadomości; wyślij kolejną wiadomość, aby kontynuować",
"ui.ai_reset": "Resetuj",
"ui.ai_settings": "Ustawienia AI…",
"ui.ai_show_features": "Pokaż funkcje AI",
@@ -1778,6 +1778,8 @@
"ui.filter_commands_or_keys": "Filtruj polecenia lub klawisze",
"ui.findInTimeline": "Znajdź na osi czasu",
"ui.findInWindow": "Znajdź w oknie",
"ui.fine": "Dokładna",
"ui.finest": "Najdokładniejsza",
"ui.finish_sketch": "Zakończ szkic",
"ui.fit": "Dopasuj",
"ui.fix_unfix": "Ustal/zwolnij",
@@ -2009,6 +2011,7 @@
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
"ui.properties": "Właściwości",
"ui.pull_direction": "Kierunek wyjmowania",
"ui.quality": "Jakość",
"ui.quantity": "Liczba",
"ui.quantity_2": "Liczba 2",
"ui.quit": "Zakończ",
@@ -2103,6 +2106,7 @@
"ui.spline": "Splajn",
"ui.split_body": "Podziel bryłę",
"ui.stand_up_tip": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X",
"ui.standard": "Standardowa",
"ui.start": "Start",
"ui.start_over": "Zacznij od nowa",
"ui.start_page": "Strona startowa",
+332 -1
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,26 +411,34 @@
"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",
"Show in folder": "Mostrar na pasta",
"Show the pointer an AI agent moves (#35)": "Mostrar o ponteiro que um agente de IA move (#35)",
"Show tool at cursor": "Mostrar a ferramenta junto ao cursor",
"Signed axes": "Eixos com sinal",
"Size": "Tamanho",
"Sketch constraints": "Restrições do esboço",
@@ -347,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.",
@@ -356,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",
@@ -378,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",
@@ -403,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",
@@ -459,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",
@@ -473,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",
@@ -595,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",
@@ -774,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",
@@ -873,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",
@@ -982,6 +1150,15 @@
"motion.to_end": "Para o fim",
"motion.to_start": "Para o início",
"motion.track_name": "Trilha {number}",
"nav.alias.help": "Shift+Alt+arrastar com o esquerdo orbita, Shift+Alt+meio desloca, Shift+Alt+direito aplica zoom; arrastar com o meio também desloca e arrastar com o direito orbita",
"nav.blender.help": "Arrastar com o meio orbita, Shift+meio desloca, Ctrl+meio aplica zoom; arrastar com o direito não mexe na câmera",
"nav.free_orbit.help": "O mouse e o mouse 3D giram passando pelos dois polos e preservam o giro lateral (roll). O encaixe em eixo com Shift fica desativado.",
"nav.free_orbit.label": "Órbita livre",
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.openscad.help": "Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
"nav.solidworks.help": "Arrastar com o meio orbita, Ctrl+meio desloca, Shift+meio aplica zoom, arrastar com o direito orbita",
"nav.tinkercad.help": "Arrastar com o direito orbita, arrastar com o meio ou Shift+direito desloca",
"nav.touchpad.help": "Segure Shift e mova o ponteiro para deslocar; segure Alt/Option e mova para orbitar. Não é preciso botão do mouse. A rolagem vertical aplica zoom.",
"nav.touchpad.label": "Touchpad",
"nav.touchpad.orbit": "Orbitar (segure Alt/Option e mova; sem botão do mouse)",
@@ -1004,8 +1181,130 @@
"pattern.flip_direction": "Inverter direção",
"pick a hole to seat it, or OK places it at the origin": "selecione um furo para assentá-la, ou OK a coloca na origem",
"pick one or two vertices, edges or faces": "selecione um ou dois vértices, arestas ou faces",
"prefs.add_item": "Adicionar o item",
"prefs.alt_frees_manipulators": "Alt / Option também libera as setas do manipulador",
"prefs.anti_aliasing_note": "Suaviza bordas serrilhadas.",
"prefs.applies_on_restart": "Aplica-se ao reiniciar",
"prefs.assistant": "Assistente",
"prefs.axes": "Eixos",
"prefs.axis": "Eixo",
"prefs.bottom": "Embaixo",
"prefs.browser": "Navegador",
"prefs.built_in": "Integrado",
"prefs.button_left": "Arrastar com o esquerdo",
"prefs.button_middle": "Arrastar com o meio",
"prefs.button_right": "Arrastar com o direito",
"prefs.choose_font": "Escolher arquivo de fonte…",
"prefs.click": "Clique",
"prefs.contact_shading": "Sombreamento suave de contato",
"prefs.contact_shading_note": "Sombreamento suave onde as peças se aproximam.",
"prefs.context_menu": "Menu de contexto",
"prefs.context_menu_ring": "Menu de contexto · segure para o menu radial",
"prefs.cpu": "CPU (software)",
"prefs.crash_reports": "Abrir relatórios de falhas…",
"prefs.crash_reports_note": "Os relatórios ficam guardados neste computador, com o projeto e os comandos que reproduzem o problema.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Superfícies curvas",
"prefs.curves": "Superfícies curvas",
"prefs.dead_zone_note": "A faixa hachurada é a zona morta: o movimento dentro dela é ignorado.",
"prefs.default": "padrão",
"prefs.design": "Padrões do projeto",
"prefs.design_note": "Com o que os novos projetos começam. Projetos abertos mantêm as próprias configurações.",
"prefs.devices": "Mouse 3D",
"prefs.display": "Exibição",
"prefs.edges": "Arestas e sombreamento",
"prefs.extend": "Extensões",
"prefs.extra_navigation": "Ctrl+arrastar com o esquerdo no espaço vazio orbita; segure Espaço para deslocar.",
"prefs.follows_os": "Segue o sistema",
"prefs.font": "Fonte",
"prefs.font_fallback": "Os glifos que faltam na fonte escolhida usam a fonte integrada, então nenhum rótulo mostra uma caixa vazia.",
"prefs.font_sample": "Vovô viu o ímã, a maçã e o pinguim · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Não foi possível ler a fonte selecionada.",
"prefs.general": "Geral",
"prefs.general_note": "Idioma, salvamento e atualizações.",
"prefs.gesture_orbit": "Orbitar",
"prefs.gesture_pan": "Deslocar",
"prefs.gesture_zoom": "Zoom",
"prefs.graphics": "Gráficos",
"prefs.graphics_note": "Troque velocidade por suavidade. Cada opção mostra seu efeito em uma amostra.",
"prefs.graphics_presets_note": "As predefinições ajustam as opções abaixo em conjunto.",
"prefs.history": "Histórico",
"prefs.hold_stop_snapping": "Segure para desativar o encaixe",
"prefs.icons_only": "Só ícones",
"prefs.imported_file": "Arquivo importado",
"prefs.input": "Entrada",
"prefs.interface": "Interface",
"prefs.interface_language": "Idioma da interface",
"prefs.interface_note": "A aparência do SolidCraft e onde ficam suas partes.",
"prefs.interval_minutes": "{n} min",
"prefs.labels": "Rótulos",
"prefs.layout": "Layout",
"prefs.layout_note": "Compacto encaixa os painéis nas bordas da janela. A translucidez do navegador está disponível em Flutuante. Mude o lado do Assistente na página Assistente.",
"prefs.left": "Esquerda",
"prefs.left_click": "Clique esquerdo",
"prefs.live_input": "Entrada ao vivo",
"prefs.live_preview": "Visualização ao vivo",
"prefs.minutes": "minutos",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Mouse e trackpad",
"prefs.mouse_heading": "Mouse",
"prefs.mouse_mapping": "Mapeamento do mouse",
"prefs.navigation": "Navegação",
"prefs.navigation_note": "Como o mouse, o trackpad e o mouse 3D movem a vista.",
"prefs.not_on_gpu": "indisponível nesta GPU",
"prefs.on": "Ativado",
"prefs.opaque": "Opaco",
"prefs.orbit_snap": "Segure Shift ao orbitar para encaixar em um eixo.",
"prefs.orientation": "Orientação",
"prefs.overall_speed": "Velocidade geral",
"prefs.preview_choices": "Prévia das suas escolhas",
"prefs.quality_default": "padrão",
"prefs.quality_fastest": "mais rápido",
"prefs.quality_slowest": "mais lento",
"prefs.quality_smoother": "mais suave",
"prefs.reports_saved": "{n} salvos",
"prefs.right_click": "Clique direito",
"prefs.saving": "Salvamento automático e recuperação",
"prefs.scale_note": "Também Ctrl + / Ctrl − na janela. Os botões têm 28 px em 100%.",
"prefs.scale_text": "Escala e texto",
"prefs.select": "Selecionar",
"prefs.select_one_item": "Selecionar um item",
"prefs.selecting": "Seleção",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift só adiciona; Ctrl / Cmd alterna",
"prefs.shift_add_note": "Ativado: Shift adiciona, Ctrl / Cmd alterna. Desativado (padrão): ambos alternam.",
"prefs.shortcuts": "Atalhos",
"prefs.sketch_selection": "Esboço e seleção",
"prefs.sketch_selection_heading": "Esboço e seleção",
"prefs.sketch_selection_note": "Como os esboços encaixam e como os cliques selecionam.",
"prefs.sketching": "Criação de esboços",
"prefs.snap_drawing_note": "Ao desenhar ou arrastar um ponto do esboço.",
"prefs.source": "Origem",
"prefs.speed": "Velocidade",
"prefs.stand_up_note": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X.",
"prefs.status_done": "Concluído",
"prefs.status_error": "Erro",
"prefs.status_selected": "Selecionado",
"prefs.status_warning": "Aviso",
"prefs.supersampling_brief": "Desenha a vista maior e a reduz com filtragem: muito nítida, e mais pesada.",
"prefs.tab_accent_note": "A cor de destaque acompanha a aba ativa",
"prefs.theme_file": "arquivo de tema",
"prefs.timeline_half_hour": "−30 min",
"prefs.timeline_now": "agora",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Adicionar o item, ou retirá-lo de novo",
"prefs.toolbar": "Barra de ferramentas",
"prefs.translucent": "Translúcido",
"prefs.under_browser": "Abaixo do navegador",
"prefs.under_cursor": "Sob o cursor",
"prefs.units_precision": "Unidades e precisão",
"prefs.updates": "Atualizações",
"prefs.view": "Comportamento da vista",
"prefs.viewport": "Janela de visualização",
"prefs.wheel": "Roda",
"prefs.wheel_trackpad": "Roda e trackpad",
"prefs.workspace": "Espaço de trabalho",
"prefs.zoom_toward_cursor": "Zoom em direção ao cursor",
"properties.title": "Propriedades: {name}",
"recovery.days_ago": "há {count} dias",
"recovery.detail": "{file} · {features} recursos · salvo automaticamente {when}",
@@ -1083,9 +1382,12 @@
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
"settings.msaa_reset": "{setting}: {requested}× não é suportado por este dispositivo gráfico; agora é {reset}× (suportados: {supported}).",
"settings.msaa_unsupported": "Não suportado por este dispositivo gráfico",
"settings.reference_geometry_through_bodies": "Mostrar geometria de referência através dos corpos",
"settings.reference_geometry_through_bodies_help": "Desenhar contornos de esboços inativos e planos de referência sobre os corpos sombreados",
"settings.reset_intro": "Estas configurações não funcionam neste computador e impediriam o SolidCraft de iniciar, por isso foram alteradas:",
"settings.reset_title": "As configurações foram alteradas",
"settings.supersampling_note": "Desenha a vista em resolução maior e a filtra para baixo: arestas, linhas e grade mais suaves em telas abaixo de 4K. Custa tempo de GPU: cerca de 2,25× os pixels em 1,5× e 4× em 2×.",
"sheet.no_body": "não há corpo de chapa metálica: comece um com Flange > Base, ou transforme uma placa em chapa metálica com Converter em chapa metálica (selecione a face plana grande dela)",
"shortcut.json_error": "os parâmetros devem estar no formato JSON: {error}",
"shortcut.no_command": "o comando `{id}` não existe; o texto antes do JSON deve ser um ID de comando",
"shortcut.pin": "Fixar no topo da caixa S",
@@ -1122,6 +1424,9 @@
"thread.designation": "Designação",
"thread.iso_metric": "Métrica ISO",
"thread.select_size": "Selecionar tamanho",
"timeline.delete_after_marker_one": "Excluir 1 recurso após o marcador do histórico",
"timeline.delete_after_marker_other": "Excluir {count} recursos após o marcador do histórico",
"timeline.delete_many": "Excluir {count} recursos selecionados",
"timeline.group": "Nova pasta de recursos",
"timeline.move_many": "Mover itens da linha do tempo",
"timeline.move_to_group": "Mover recursos para a pasta",
@@ -1367,11 +1672,15 @@
"ui.agent_chat_waiting": "Aguardando sua resposta",
"ui.agent_cursor": "Cursor do agente",
"ui.agent_cursor_speed": "Velocidade do cursor do agente",
"ui.agent_off_thread": "Manter a janela respondendo enquanto os agentes executam comandos",
"ui.agent_off_thread_note": "Os comandos lentos de um agente (do painel de IA ou de um cliente MCP) são calculados ao lado da janela: você pode continuar girando e inspecionando a vista, e parar um deles pelo indicador na vista. Desativado: cada um roda na thread da janela, e a janela espera ele terminar.",
"ui.ai_add": "Adicionar",
"ui.ai_add_agent": "Adicionar agente…",
"ui.ai_add_agent_title": "Adicionar agente",
"ui.ai_add_anthropic": "API compatível com Anthropic",
"ui.ai_add_api": "API compatível com OpenAI",
"ui.ai_add_custom": "Comando personalizado",
"ui.ai_add_endpoint": "Endpoint de API",
"ui.ai_add_installed": "Agente instalado",
"ui.ai_add_installed_note": "Uma entrada própria para um agente instalado, por exemplo com outros argumentos. Os agentes instalados são listados por si mesmos.",
"ui.ai_added": "adicionado",
@@ -1400,6 +1709,7 @@
"ui.ai_key_needs_https": "Uma chave só é enviada por HTTPS ou para este computador.",
"ui.ai_key_note": "Guardada no chaveiro do sistema, nunca nas configurações do SolidCraft. Deixe em branco para um servidor local.",
"ui.ai_keychain_error": "O chaveiro do sistema recusou: {error}",
"ui.ai_kind": "Tipo",
"ui.ai_mode_all": "Permitir tudo",
"ui.ai_mode_all_note": "Nada pergunta antes. Os relatórios de falha continuam fora de alcance e os limites de pasta ainda valem.",
"ui.ai_mode_all_warning": "O agente pode salvar, sobrescrever, exportar e substituir seu projeto sem perguntar. Use isto só com um agente em que você confie e em projetos que você já salvou.",
@@ -1421,8 +1731,8 @@
"ui.ai_panel": "Painel",
"ui.ai_permissions": "Permissões",
"ui.ai_preset": "Predefinição",
"ui.ai_protocol": "Protocolo",
"ui.ai_remove": "Remover",
"ui.ai_request_limit": "parou após 24 requisições para uma mensagem; envie outra mensagem para continuar",
"ui.ai_reset": "Redefinir",
"ui.ai_settings": "Configurações de IA…",
"ui.ai_show_features": "Mostrar recursos de IA",
@@ -1431,6 +1741,8 @@
"ui.ai_test": "Testar",
"ui.ai_test_acp_ok": "Responde: {agent}",
"ui.ai_test_acp_tip": "Inicia o agente por um instante e pede uma sessão; depois o encerra",
"ui.ai_test_answered": "Acessado: {model} respondeu",
"ui.ai_test_anthropic_tip": "Envia uma mensagem curta ao endpoint",
"ui.ai_test_api_tip": "Pede ao endpoint a lista de modelos",
"ui.ai_test_no_model": "Acessado: {count} modelos, mas não {model}",
"ui.ai_test_ok": "Acessado: {count} modelos",
@@ -1589,6 +1901,9 @@
"ui.export_keymap_tip": "Teclas, favoritos da caixa S e a barra de ferramentas, como um arquivo",
"ui.extend": "Estender",
"ui.extent_type": "Tipo de extensão",
"ui.extract_component": "Extrair para arquivo…",
"ui.extract_design_changed": "O projeto mudou enquanto um arquivo era escolhido. Inicie Extrair para arquivo de novo.",
"ui.extract_done": "O componente agora é um projeto próprio, vinculado aqui.",
"ui.extrude": "Extrudar",
"ui.factor": "Fator",
"ui.feature_history": "Histórico de recursos",
@@ -1599,6 +1914,8 @@
"ui.filter_commands_or_keys": "Filtrar comandos ou teclas",
"ui.findInTimeline": "Localizar na linha do tempo",
"ui.findInWindow": "Localizar na janela",
"ui.fine": "Fina",
"ui.finest": "Muito fina",
"ui.finish_sketch": "Concluir esboço",
"ui.fit": "Ajustar",
"ui.fix_unfix": "Fixar/Liberar",
@@ -1696,10 +2013,17 @@
"ui.hideProfile": "Ocultar perfil",
"ui.history_back": "Voltar uma etapa",
"ui.history_below_browser": "Abaixo do navegador",
"ui.history_edge_hint": "clique duas vezes na borda",
"ui.history_first": "Ir para o início",
"ui.history_forward": "Avançar uma etapa",
"ui.history_hide_names": "Ocultar nomes",
"ui.history_last": "Ir para o fim",
"ui.history_marker_tip": "Marcador do histórico: arraste para retroceder o modelo; novos recursos entram aqui",
"ui.history_names": "Nomes",
"ui.history_play": "Reproduzir até o fim",
"ui.history_show_names": "Mostrar nomes",
"ui.history_title": "Histórico",
"ui.history_toggle_names": "Mostrar ou ocultar os nomes dos recursos",
"ui.hole": "Furo",
"ui.hole_tap_type": "Tipo de rosca do furo",
"ui.hole_type": "Tipo de furo",
@@ -1721,6 +2045,10 @@
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
"ui.link_copies": "Vincular cópias idênticas",
"ui.link_copies_keep": "Manter",
"ui.link_copies_link": "Vincular",
"ui.link_copies_text": "Estes componentes constroem os mesmos corpos que {name}. Torná-los posicionamentos do vínculo dele?",
"ui.lock": "Bloquear",
"ui.loft": "Loft",
"ui.look_at": "Olhar para",
@@ -1823,6 +2151,7 @@
"ui.project_face_edges_sketch": "Projetar arestas da face em novos esboços",
"ui.properties": "Propriedades",
"ui.pull_direction": "Direção de extração",
"ui.quality": "Qualidade",
"ui.quantity": "Quantidade",
"ui.quantity_2": "Quantidade 2",
"ui.quit": "Sair",
@@ -1844,6 +2173,7 @@
"ui.render": "Renderizar",
"ui.renderer": "Renderizador",
"ui.repeat": "Repetir",
"ui.replace_with_link_done": "As cópias agora são posicionamentos do projeto vinculado.",
"ui.report_a_bug": "Relatar um bug",
"ui.reset": "Redefinir",
"ui.reset_all": "Redefinir tudo",
@@ -1917,6 +2247,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dividir corpo",
"ui.stand_up_tip": "Depois que um arquivo STEP ou IGES abrir com Z para cima, oferecer um quarto de volta em torno de X",
"ui.standard": "Padrão",
"ui.start": "Início",
"ui.start_over": "Recomeçar",
"ui.start_page": "Página inicial",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Afișează cursorul agentului",
"Show in folder": "Afișează în dosar",
"Show the pointer an AI agent moves (#35)": "Afișează indicatorul pe care îl mută un agent AI (#35)",
"Show tool at cursor": "Afișează instrumentul lângă cursor",
"Signed axes": "Axe cu semn",
"Size": "Dimensiune",
"Sketch constraints": "Constrângerile schiței",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "Permisiuni",
"ui.ai_preset": "Presetare",
"ui.ai_remove": "Elimină",
"ui.ai_request_limit": "oprit după 24 de cereri pentru un mesaj; trimiteți alt mesaj pentru a continua",
"ui.ai_reset": "Resetează",
"ui.ai_settings": "Setări AI…",
"ui.ai_show_features": "Afișează funcțiile AI",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filtrează comenzi sau taste",
"ui.findInTimeline": "Găsește în cronologie",
"ui.findInWindow": "Găsește în fereastră",
"ui.fine": "Fină",
"ui.finest": "Foarte fină",
"ui.finish_sketch": "Finalizează schița",
"ui.fit": "Potrivire",
"ui.fix_unfix": "Fixează/Defixează",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Proiectează muchiile feței în schițe noi",
"ui.properties": "Proprietăți",
"ui.pull_direction": "Direcția de extragere",
"ui.quality": "Calitate",
"ui.quantity": "Cantitate",
"ui.quantity_2": "Cantitatea 2",
"ui.quit": "Ieșire",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Împarte corpul",
"ui.stand_up_tip": "După ce un fișier STEP sau IGES se deschide cu Z în sus, oferă un sfert de rotație în jurul lui X",
"ui.standard": "Standard",
"ui.start": "Început",
"ui.start_over": "Reîncepe",
"ui.start_page": "Pagina de start",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Показывать курсор агента",
"Show in folder": "Показать в папке",
"Show the pointer an AI agent moves (#35)": "Показывать указатель, которым двигается ИИ-агент (#35)",
"Show tool at cursor": "Показывать инструмент у курсора",
"Signed axes": "Знаковые оси",
"Size": "Размер",
"Sketch constraints": "Ограничения эскиза",
@@ -1414,7 +1415,6 @@
"ui.ai_permissions": "Разрешения",
"ui.ai_preset": "Пресет",
"ui.ai_remove": "Удалить",
"ui.ai_request_limit": "остановлено после 24 запросов на одно сообщение; отправьте ещё одно сообщение, чтобы продолжить",
"ui.ai_reset": "Сбросить",
"ui.ai_settings": "Настройки ИИ…",
"ui.ai_show_features": "Показывать функции ИИ",
@@ -1591,6 +1591,8 @@
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
"ui.findInTimeline": "Найти на шкале",
"ui.findInWindow": "Найти в окне",
"ui.fine": "Высокое",
"ui.finest": "Максимальное",
"ui.finish_sketch": "Завершить эскиз",
"ui.fit": "Вписать",
"ui.fix_unfix": "Зафиксировать/Освободить",
@@ -1815,6 +1817,7 @@
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
"ui.properties": "Свойства",
"ui.pull_direction": "Направление вытягивания",
"ui.quality": "Качество",
"ui.quantity": "Количество",
"ui.quantity_2": "Количество 2",
"ui.quit": "Выход",
@@ -1909,6 +1912,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Разделить тело",
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
"ui.standard": "Стандартное",
"ui.start": "Старт",
"ui.start_over": "Начать заново",
"ui.start_page": "Начальная страница",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Visa agentmarkör",
"Show in folder": "Visa i mapp",
"Show the pointer an AI agent moves (#35)": "Visa pekaren som en AI-agent flyttar (#35)",
"Show tool at cursor": "Visa verktyget vid markören",
"Signed axes": "Axlar med tecken",
"Size": "Storlek",
"Sketch constraints": "Skissvillkor",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "Behörigheter",
"ui.ai_preset": "Förinställning",
"ui.ai_remove": "Ta bort",
"ui.ai_request_limit": "stoppades efter 24 förfrågningar för ett meddelande; skicka ett nytt meddelande för att fortsätta",
"ui.ai_reset": "Återställ",
"ui.ai_settings": "AI-inställningar…",
"ui.ai_show_features": "Visa AI-funktioner",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "Filtrera kommandon eller tangenter",
"ui.findInTimeline": "Sök i tidslinjen",
"ui.findInWindow": "Sök i fönstret",
"ui.fine": "Fin",
"ui.finest": "Finast",
"ui.finish_sketch": "Avsluta skiss",
"ui.fit": "Anpassa",
"ui.fix_unfix": "Fäst/Lös",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "Projicera ytkanter i nya skisser",
"ui.properties": "Egenskaper",
"ui.pull_direction": "Dragriktning",
"ui.quality": "Kvalitet",
"ui.quantity": "Antal",
"ui.quantity_2": "Antal 2",
"ui.quit": "Avsluta",
@@ -1852,6 +1855,7 @@
"ui.spline": "Spline",
"ui.split_body": "Dela kropp",
"ui.stand_up_tip": "När en STEP- eller IGES-fil öppnas med Z uppåt, erbjud ett kvartsvarv runt X",
"ui.standard": "Standard",
"ui.start": "Start",
"ui.start_over": "Börja om",
"ui.start_page": "Startsida",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "Ajan imlecini göster",
"Show in folder": "Klasörde göster",
"Show the pointer an AI agent moves (#35)": "Bir AI ajanının hareket ettirdiği imleci göster (#35)",
"Show tool at cursor": "Aracı imlecin yanında göster",
"Signed axes": "İşaretli eksenler",
"Size": "Boyut",
"Sketch constraints": "Eskiz kısıtlamaları",
@@ -1414,7 +1415,6 @@
"ui.ai_permissions": "İzinler",
"ui.ai_preset": "Şablon",
"ui.ai_remove": "Kaldır",
"ui.ai_request_limit": "bir mesaj için 24 istekten sonra durduruldu; devam etmek için başka bir mesaj gönderin",
"ui.ai_reset": "Sıfırla",
"ui.ai_settings": "AI ayarları…",
"ui.ai_show_features": "AI özelliklerini göster",
@@ -1591,6 +1591,8 @@
"ui.filter_commands_or_keys": "Komutları veya tuşları filtrele",
"ui.findInTimeline": "Zaman çizelgesinde bul",
"ui.findInWindow": "Pencerede bul",
"ui.fine": "İnce",
"ui.finest": "En ince",
"ui.finish_sketch": "Eskizi bitir",
"ui.fit": "Sığdır",
"ui.fix_unfix": "Sabitle/Çöz",
@@ -1815,6 +1817,7 @@
"ui.project_face_edges_sketch": "Yüz kenarlarını yeni eskizlere yansıt",
"ui.properties": "Özellikler",
"ui.pull_direction": "Kalıptan çıkarma yönü",
"ui.quality": "Kalite",
"ui.quantity": "Adet",
"ui.quantity_2": "Adet 2",
"ui.quit": "Çıkış",
@@ -1909,6 +1912,7 @@
"ui.spline": "Spline",
"ui.split_body": "Gövdeyi böl",
"ui.stand_up_tip": "Bir STEP veya IGES dosyası Z yukarı olarak açıldığında X etrafında çeyrek tur döndürmeyi öner",
"ui.standard": "Standart",
"ui.start": "Başlangıç",
"ui.start_over": "Baştan başla",
"ui.start_page": "Başlangıç sayfası",
+308 -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": "Величина",
"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,26 +410,34 @@
"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": "Показати курсор агента",
"Show in folder": "Показати в теці",
"Show the pointer an AI agent moves (#35)": "Показувати вказівник, яким рухає ШІ-агент (#35)",
"Show tool at cursor": "Показувати інструмент біля курсора",
"Signed axes": "Підписані осі",
"Size": "Розмір",
"Sketch constraints": "Обмеження ескізу",
@@ -347,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 як одна операція переміщення, яку можна скасувати.",
@@ -356,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": "Потім",
@@ -378,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": "Навігатор виду",
@@ -403,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": "Металевість",
@@ -769,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": "Клацніть, щоб скопіювати значення кольору",
@@ -866,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": "Звіти про збої",
@@ -975,6 +1138,15 @@
"motion.to_end": "У кінець",
"motion.to_start": "На початок",
"motion.track_name": "Доріжка {number}",
"nav.alias.help": "Shift+Alt+ліва обертає, Shift+Alt+середня панорамує, Shift+Alt+права масштабує; середня також панорамує, права обертає",
"nav.blender.help": "Середня обертає, Shift+середня панорамує, Ctrl+середня масштабує; права не рухає камеру",
"nav.free_orbit.help": "Миша та 3D-миша обертають через обидва полюси й зберігають крен. Прив'язування до осей через Shift вимкнено.",
"nav.free_orbit.label": "Вільне обертання",
"nav.fusion.help": "Середня панорамує, Shift+середня обертає, права обертає",
"nav.inventor.help": "Середня панорамує, Shift+середня обертає, Ctrl+середня масштабує, права обертає",
"nav.openscad.help": "Ліва обертає по порожньому місцю, Shift+ліва виділяє рамкою, геометрію ескізу перетягують для редагування; права або середня панорамує, Shift+права або Shift+середня масштабує",
"nav.solidworks.help": "Середня обертає, Ctrl+середня панорамує, Shift+середня масштабує, права обертає",
"nav.tinkercad.help": "Права обертає, середня або Shift+права панорамує",
"nav.touchpad.help": "Утримуйте Shift і рухайте вказівник, щоб панорамувати; утримуйте Alt/Option і рухайте, щоб обертати. Кнопка миші не потрібна. Вертикальна прокрутка масштабує.",
"nav.touchpad.label": "Трекпад",
"nav.touchpad.orbit": "Обертання (утримуйте Alt/Option і рухайте; без кнопки миші)",
@@ -997,8 +1169,130 @@
"pattern.flip_direction": "Змінити напрямок",
"pick a hole to seat it, or OK places it at the origin": "виберіть отвір для посадки, або OK розмістить його в початку координат",
"pick one or two vertices, edges or faces": "виберіть одну-дві вершини, ребра або грані",
"prefs.add_item": "Додати елемент",
"prefs.alt_frees_manipulators": "Alt / Option також дає вільне перетягування стрілок маніпулятора",
"prefs.anti_aliasing_note": "Згладжує нерівні краї.",
"prefs.applies_on_restart": "Застосовується після перезапуску",
"prefs.assistant": "Асистент",
"prefs.axes": "Осі",
"prefs.axis": "Вісь",
"prefs.bottom": "Низ",
"prefs.browser": "Браузер",
"prefs.built_in": "Вбудована",
"prefs.button_left": "Перетягування лівою кнопкою",
"prefs.button_middle": "Перетягування середньою кнопкою",
"prefs.button_right": "Перетягування правою кнопкою",
"prefs.choose_font": "Вибрати файл шрифту…",
"prefs.click": "Клацання",
"prefs.contact_shading": "М'яке контактне затінення",
"prefs.contact_shading_note": "М'яке затінення там, де деталі зближуються.",
"prefs.context_menu": "Контекстне меню",
"prefs.context_menu_ring": "Контекстне меню · утримуйте для кругового меню",
"prefs.cpu": "ЦП (програмний)",
"prefs.crash_reports": "Відкрити звіти про збої…",
"prefs.crash_reports_note": "Звіти зберігаються на цьому комп'ютері разом із проєктом і командами, що відтворюють проблему.",
"prefs.ctrl_cmd": "Ctrl / Cmd",
"prefs.curved_surfaces": "Криві поверхні",
"prefs.curves": "Криві поверхні",
"prefs.dead_zone_note": "Заштрихована смуга — це мертва зона: рух у її межах ігнорується.",
"prefs.default": "типово",
"prefs.design": "Типові параметри проєкту",
"prefs.design_note": "З чого починаються нові проєкти. Відкриті проєкти зберігають власні параметри.",
"prefs.devices": "3D-миша",
"prefs.display": "Відображення",
"prefs.edges": "Ребра та затінення",
"prefs.extend": "Розширення",
"prefs.extra_navigation": "Ctrl+перетягування лівою на порожньому місці обертає; утримуйте Space, щоб панорамувати.",
"prefs.follows_os": "Як у системі",
"prefs.font": "Шрифт",
"prefs.font_fallback": "Символи, яких немає у вибраному шрифті, беруться з вбудованого, тож жоден напис не показує порожній квадрат.",
"prefs.font_sample": "Ґедзь, що їв фінік, з'їв і юшку · Ø12.50 mm · 45.0° · 0123456789",
"prefs.font_unreadable": "Не вдалося прочитати вибраний шрифт.",
"prefs.general": "Загальні",
"prefs.general_note": "Мова, збереження та оновлення.",
"prefs.gesture_orbit": "Обертання",
"prefs.gesture_pan": "Панорамування",
"prefs.gesture_zoom": "Масштаб",
"prefs.graphics": "Графіка",
"prefs.graphics_note": "Обміняйте швидкість на плавність. Кожен варіант показує свій ефект на зразку.",
"prefs.graphics_presets_note": "Набори задають наведені нижче параметри разом.",
"prefs.history": "Історія",
"prefs.hold_stop_snapping": "Утримуйте, щоб вимкнути прив'язування",
"prefs.icons_only": "Лише піктограми",
"prefs.imported_file": "Імпортований файл",
"prefs.input": "Введення",
"prefs.interface": "Інтерфейс",
"prefs.interface_language": "Мова інтерфейсу",
"prefs.interface_note": "Як виглядає SolidCraft і де розташовані його частини.",
"prefs.interval_minutes": "{n} хв",
"prefs.labels": "Підписи",
"prefs.layout": "Компонування",
"prefs.layout_note": "«Компактне» прикріплює панелі до країв вікна. Прозорість браузера доступна в режимі «Плаваюче». Сторону Асистента можна змінити на сторінці «Асистент».",
"prefs.left": "Ліворуч",
"prefs.left_click": "Клацання лівою",
"prefs.live_input": "Поточне введення",
"prefs.live_preview": "Попередній перегляд у реальному часі",
"prefs.minutes": "хв",
"prefs.minutes_suffix": " min",
"prefs.mouse": "Миша й трекпад",
"prefs.mouse_heading": "Миша",
"prefs.mouse_mapping": "Призначення кнопок миші",
"prefs.navigation": "Навігація",
"prefs.navigation_note": "Як миша, трекпад і 3D-миша рухають вид.",
"prefs.not_on_gpu": "не на цьому GPU",
"prefs.on": "Увімкнено",
"prefs.opaque": "Непрозорий",
"prefs.orbit_snap": "Утримуйте Shift під час обертання, щоб прив'язатися до осі.",
"prefs.orientation": "Орієнтація",
"prefs.overall_speed": "Загальна швидкість",
"prefs.preview_choices": "Перегляд ваших виборів",
"prefs.quality_default": "типово",
"prefs.quality_fastest": "найшвидше",
"prefs.quality_slowest": "найповільніше",
"prefs.quality_smoother": "плавніше",
"prefs.reports_saved": "Збережено: {n}",
"prefs.right_click": "Клацання правою",
"prefs.saving": "Автозбереження й відновлення",
"prefs.scale_note": "Також Ctrl + / Ctrl − у вікні. Кнопки мають 28 px за 100%.",
"prefs.scale_text": "Масштаб і текст",
"prefs.select": "Вибрати",
"prefs.select_one_item": "Вибрати один елемент",
"prefs.selecting": "Вибір",
"prefs.shift": "Shift",
"prefs.shift_add_ctrl_toggle": "Shift лише додає; Ctrl / Cmd перемикає",
"prefs.shift_add_note": "Увімкнено: Shift додає, Ctrl / Cmd перемикає. Вимкнено (типово): обидва перемикають.",
"prefs.shortcuts": "Комбінації клавіш",
"prefs.sketch_selection": "Ескіз і вибір",
"prefs.sketch_selection_heading": "Ескіз і вибір",
"prefs.sketch_selection_note": "Як ескізи прив'язуються й як клацання вибирають.",
"prefs.sketching": "Ескізування",
"prefs.snap_drawing_note": "Під час малювання або перетягування точки ескізу.",
"prefs.source": "Джерело",
"prefs.speed": "Швидкість",
"prefs.stand_up_note": "Коли файл STEP або IGES відкрито з віссю Z угору, пропонувати поворот на чверть оберту навколо X.",
"prefs.status_done": "Готово",
"prefs.status_error": "Помилка",
"prefs.status_selected": "Вибрано",
"prefs.status_warning": "Попередження",
"prefs.supersampling_brief": "Малює вид у більшому розмірі й зменшує його: дуже чітко, але потребує більше ресурсів.",
"prefs.tab_accent_note": "Акцентний колір відповідає активній вкладці",
"prefs.theme_file": "файл теми",
"prefs.timeline_half_hour": "−30 хв",
"prefs.timeline_now": "зараз",
"prefs.timeline_quarter_hour": "−15",
"prefs.toggle_item": "Додати або прибрати елемент",
"prefs.toolbar": "Панель інструментів",
"prefs.translucent": "Напівпрозорий",
"prefs.under_browser": "Під браузером",
"prefs.under_cursor": "Під курсором",
"prefs.units_precision": "Одиниці й точність",
"prefs.updates": "Оновлення",
"prefs.view": "Поведінка виду",
"prefs.viewport": "Вікно перегляду",
"prefs.wheel": "Колесо",
"prefs.wheel_trackpad": "Колесо й трекпад",
"prefs.workspace": "Робоча область",
"prefs.zoom_toward_cursor": "Масштаб до курсора",
"properties.title": "Властивості: {name}",
"recovery.days_ago": "{count} дн. тому",
"recovery.detail": "{file} · елементів: {features} · автозбережено {when}",
@@ -1360,6 +1654,8 @@
"ui.agent_chat_waiting": "Очікує на вашу відповідь",
"ui.agent_cursor": "Курсор агента",
"ui.agent_cursor_speed": "Швидкість курсора агента",
"ui.agent_off_thread": "Підтримувати чуйність вікна, поки агенти виконують команди",
"ui.agent_off_thread_note": "Повільні команди агента (з панелі ШІ або клієнта MCP) обчислюються паралельно з вікном: ви можете далі обертати й оглядати вид і зупинити команду з індикатора у виді. Вимкнено: кожна виконується в потоці вікна, і вікно чекає на неї.",
"ui.ai_add": "Додати",
"ui.ai_add_agent": "Додати агента…",
"ui.ai_add_agent_title": "Додати агента",
@@ -1415,7 +1711,6 @@
"ui.ai_permissions": "Дозволи",
"ui.ai_preset": "Набір",
"ui.ai_remove": "Видалити",
"ui.ai_request_limit": "зупинено після 24 запитів на одне повідомлення; надішліть інше повідомлення, щоб продовжити",
"ui.ai_reset": "Скинути",
"ui.ai_settings": "Налаштування ШІ…",
"ui.ai_show_features": "Показувати функції ШІ",
@@ -1592,6 +1887,8 @@
"ui.filter_commands_or_keys": "Фільтрувати команди або клавіші",
"ui.findInTimeline": "Знайти в часовому ланцюжку",
"ui.findInWindow": "Знайти у вікні",
"ui.fine": "Висока",
"ui.finest": "Максимальна",
"ui.finish_sketch": "Завершити ескіз",
"ui.fit": "Вписати",
"ui.fix_unfix": "Закріпити/розкріпити",
@@ -1689,10 +1986,17 @@
"ui.hideProfile": "Приховати профіль",
"ui.history_back": "На один крок назад",
"ui.history_below_browser": "Під браузером",
"ui.history_edge_hint": "двічі клацніть край",
"ui.history_first": "Перейти на початок",
"ui.history_forward": "На один крок уперед",
"ui.history_hide_names": "Приховати назви",
"ui.history_last": "Перейти в кінець",
"ui.history_marker_tip": "Маркер історії: перетягніть, щоб відкотити модель назад; нові елементи додаються тут",
"ui.history_names": "Назви",
"ui.history_play": "Відтворити до кінця",
"ui.history_show_names": "Показати назви",
"ui.history_title": "Історія",
"ui.history_toggle_names": "Показувати або приховувати назви елементів",
"ui.hole": "Отвір",
"ui.hole_tap_type": "Тип різьби отвору",
"ui.hole_type": "Тип отвору",
@@ -1816,6 +2120,7 @@
"ui.project_face_edges_sketch": "Проєктувати ребра граней у нові ескізи",
"ui.properties": "Властивості",
"ui.pull_direction": "Напрямок витягування",
"ui.quality": "Якість",
"ui.quantity": "Кількість",
"ui.quantity_2": "Кількість 2",
"ui.quit": "Вихід",
@@ -1910,6 +2215,7 @@
"ui.spline": "Сплайн",
"ui.split_body": "Розділити тіло",
"ui.stand_up_tip": "Після відкриття файлу STEP або IGES з напрямком Z вгору пропонувати поворот на чверть оберту навколо X",
"ui.standard": "Стандартна",
"ui.start": "Початок",
"ui.start_over": "Почати спочатку",
"ui.start_page": "Початкова сторінка",
+5 -1
View File
@@ -335,6 +335,7 @@
"Show Agent Cursor": "Hiển thị con trỏ của AI Agent",
"Show in folder": "Hiển thị trong thư mục",
"Show the pointer an AI agent moves (#35)": "Hiển thị con trỏ do AI Agent điều khiển (#35)",
"Show tool at cursor": "Hiển thị công cụ cạnh con trỏ",
"Signed axes": "Các trục có dấu (+/-)",
"Size": "Kích thước",
"Sketch constraints": "Ràng buộc phác thảo",
@@ -1415,7 +1416,6 @@
"ui.ai_permissions": "Quyền hạn",
"ui.ai_preset": "Mẫu cấu hình sẵn",
"ui.ai_remove": "Xóa",
"ui.ai_request_limit": "đã dừng sau 24 yêu cầu cho một tin nhắn; gửi tin nhắn khác để tiếp tục",
"ui.ai_reset": "Đặt lại",
"ui.ai_settings": "AI cài đặt…",
"ui.ai_show_features": "Hiển thị các tính năng AI",
@@ -1592,6 +1592,8 @@
"ui.filter_commands_or_keys": "Lọc lệnh hoặc phím tắt",
"ui.findInTimeline": "Tìm trong lịch sử thiết kế",
"ui.findInWindow": "Tìm trong cửa sổ",
"ui.fine": "Mịn",
"ui.finest": "Mịn nhất",
"ui.finish_sketch": "Hoàn tất phác thảo",
"ui.fit": "Vừa vặn (Fit)",
"ui.fix_unfix": "Cố định/Uncố định",
@@ -1816,6 +1818,7 @@
"ui.project_face_edges_sketch": "Chiếu các cạnh bề mặt vào phác thảo mới",
"ui.properties": "Thuộc tính cơ lý",
"ui.pull_direction": "Hướng kéo",
"ui.quality": "Chất lượng",
"ui.quantity": "Số lượng",
"ui.quantity_2": "Số lượng 2",
"ui.quit": "Thoát ứng dụng",
@@ -1910,6 +1913,7 @@
"ui.spline": "Đường cong Spline",
"ui.split_body": "Chia tách khối",
"ui.stand_up_tip": "Sau khi mở tệp STEP hoặc IGES với trục Z hướng lên, đề xuất xoay 90° quanh trục X",
"ui.standard": "Tiêu chuẩn",
"ui.start": "Bắt đầu",
"ui.start_over": "Bắt đầu lại",
"ui.start_page": "Trang bắt đầu",
+5 -1
View File
@@ -334,6 +334,7 @@
"Show Agent Cursor": "顯示代理游標",
"Show in folder": "在資料夾中顯示",
"Show the pointer an AI agent moves (#35)": "顯示 AI 代理移動的指標(#35)",
"Show tool at cursor": "在游標旁顯示工具",
"Signed axes": "帶符號軸",
"Size": "大小",
"Sketch constraints": "草圖拘束條件",
@@ -1358,7 +1359,6 @@
"ui.ai_permissions": "權限",
"ui.ai_preset": "預設",
"ui.ai_remove": "移除",
"ui.ai_request_limit": "每條訊息 24 次請求後已停止;再傳送一條訊息即可繼續",
"ui.ai_reset": "重設",
"ui.ai_settings": "AI 設定…",
"ui.ai_show_features": "顯示 AI 功能",
@@ -1534,6 +1534,8 @@
"ui.filter_commands_or_keys": "篩選命令或按鍵",
"ui.findInTimeline": "在時間軸中查詢",
"ui.findInWindow": "在視窗中查詢",
"ui.fine": "精細",
"ui.finest": "最精細",
"ui.finish_sketch": "完成草圖",
"ui.fit": "適合",
"ui.fix_unfix": "固定/取消固定",
@@ -1758,6 +1760,7 @@
"ui.project_face_edges_sketch": "將面的邊投影到新草圖",
"ui.properties": "屬性",
"ui.pull_direction": "拔模方向",
"ui.quality": "品質",
"ui.quantity": "數量",
"ui.quantity_2": "數量 2",
"ui.quit": "結束",
@@ -1852,6 +1855,7 @@
"ui.spline": "雲形線",
"ui.split_body": "分割實體",
"ui.stand_up_tip": "當 Z 朝上的 STEP 或 IGES 檔案開啟後,提示繞 X 軸旋轉四分之一週",
"ui.standard": "標準",
"ui.start": "開始",
"ui.start_over": "重新開始",
"ui.start_page": "起始頁面",
+120 -1
View File
@@ -8,6 +8,7 @@
"A Single hole snaps to the middle of its face and to the centres of its round edges": "单个孔捕捉到其所在面的中心和圆形边的中心",
"A new configuration, a copy of the active one": "新建配置,为当前配置的副本",
"A placed Single hole has a handle to drag it, and picking its face again moves the hole there instead of taking it out": "已放置的单个孔带有可拖动的控点,再次选择该面会将孔移到该处而不是删除它",
"A short title for the problem (optional)": "问题的简短标题(可选)",
"ARRANGE": "排列",
"About SolidCraft": "关于 SolidCraft",
"Acceleration": "加速度",
@@ -21,6 +22,7 @@
"Add Mid Radius": "添加中间半径",
"Add Row": "添加行",
"Add a tool": "添加工具",
"Add bend": "添加折弯",
"Add condition": "添加条件",
"Add or edit rule": "添加或编辑规则",
"Add to New Folder": "添加到新文件夹",
@@ -35,7 +37,9 @@
"Also deletes": "同时删除",
"Anchor": "基准点",
"Angle 2": "角度 2",
"Angle Display": "角度显示",
"Animate": "动画",
"Another bend on the far edge of the last flange": "在最后一个法兰的远端边上再加一道折弯",
"App/system information and command settings are included. Review private details before sharing.": "报告包含应用和系统信息以及命令设置。共享前请检查私人信息。",
"Appearance name": "外观名称",
"Applied Radius": "应用半径",
@@ -52,6 +56,7 @@
"BOUNDARY SURFACE": "边界曲面",
"BROWSER": "浏览器",
"Bar": "扁钢",
"Bend": "折弯角度",
"Bend Edges": "折弯边",
"Bend Line Position": "弯折线位置",
"Bend Position": "折弯位置",
@@ -138,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)",
@@ -218,6 +224,7 @@
"Fit and G2 corners require Constant radius.": "拟合和 G2 拐角需要 Constant radius。",
"Fixed Face": "固定面",
"Flange thickness": "翼缘厚度",
"Flanges": "法兰",
"Flat Pattern": "展开图",
"Flat size": "平板尺寸",
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
@@ -227,11 +234,15 @@
"From Bodies": "从实体创建",
"From edges": "起始边",
"Full Length": "全长",
"Full thread depth": "完整螺纹深度",
"Further Bends": "更多折弯",
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 拐角需要恒定半径和 Exact fit。尚不支持构建 G2 拐角。",
"Gap": "间隙",
"Grid": "网格",
"HISTORY": "历史",
"Halo under sketch curves": "在草图曲线下方显示光晕",
"Height": "高度",
"Height From": "高度基准",
"Hidden edges": "隐藏线",
"History layout": "历史布局",
"History names": "历史名称",
@@ -247,6 +258,8 @@
"In This Design": "在此设计中",
"In place (surfaces)": "原位(曲面)",
"In plane": "在平面内",
"Included": "夹角",
"Inner Sharp": "内侧虚拟尖点",
"Inside": "内侧",
"Instant": "即时",
"Interface font": "界面字体",
@@ -293,6 +306,7 @@
"Mass Center": "质心",
"Matched restrictions check the final values.": "满足条件的限制规则检查最终值。",
"Material": "材质",
"Material Outside": "材料在外",
"Material copied. Existing body assignments are unchanged.": "材质已复制。现有实体指定保持不变。",
"Max": "最大",
"Maximum": "最大",
@@ -332,14 +346,17 @@
"Nominal diameter": "公称直径",
"None": "无",
"Not available yet": "暂不可用",
"Not saved yet": "尚未保存",
"Notch": "缺口",
"OUTPUT": "输出",
"Of": "的值",
"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中检查并提交报告。",
@@ -347,6 +364,8 @@
"Orbit (right drag)": "旋转(右键拖动)",
"Other side": "另一侧",
"Other surface (mutual trim)": "另一曲面(相互修剪)",
"Other way": "反方向",
"Outer Sharp": "外侧虚拟尖点",
"Outer radius": "外圆角半径",
"Outside": "外侧",
"Overall length": "总长",
@@ -402,6 +421,7 @@
"Remove screenshot": "移除截图",
"Remove the column": "移除此列",
"Remove the track": "移除轨迹",
"Remove this bend": "移除此折弯",
"Remove this point": "移除此点",
"Rename Feature": "重命名特征",
"Rename Folder": "重命名文件夹",
@@ -443,6 +463,7 @@
"Rules run from top to bottom. Matched drives change effective values.": "规则从上到下依次运行。满足条件的驱动规则更改有效值。",
"SECTION": "剖面",
"SURFACE QUALITY": "曲面质量",
"Same way": "同方向",
"Save Rule": "保存规则",
"Save View": "保存视图",
"Save screenshot…": "保存屏幕截图…",
@@ -471,8 +492,10 @@
"Shape": "形状",
"Sheet Rules": "钣金规则",
"Show Agent Cursor": "显示代理光标",
"Show ghost": "显示虚影",
"Show in folder": "在文件夹中显示",
"Show the pointer an AI agent moves (#35)": "显示 AI 代理移动的指针(#35)",
"Show tool at cursor": "在光标旁显示工具",
"Signed axes": "带符号轴",
"Size": "大小",
"Sketch constraints": "草图约束",
@@ -509,9 +532,11 @@
"Surface, or faces to turn over": "曲面,或要翻转的面",
"Swap Ends": "交换端点",
"TRIM": "修剪",
"Tangent": "相切",
"Tangent (G1)": "相切 (G1)",
"Tangent Chain": "相切链",
"Tangent chain": "相切链",
"Tangent to Edge": "与边相切",
"Tap drill diameter": "攻丝底孔直径",
"Target": "目标",
"Target Body": "目标实体",
@@ -529,6 +554,7 @@
"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中附加。",
"Thread depth": "螺纹深度",
"Thread diameter": "螺纹直径",
"To": "终点",
"To Offset": "偏移",
@@ -588,10 +614,13 @@
"Z angle": "Z 角度",
"Z distance": "Z 距离",
"Zebra": "斑马纹",
"ai.connected_toast": "SolidCraft 现已可在 {clients} 中使用。请重启它们(设置 > 助手)。",
"ai.prefs.allow_control": "允许 AI 代理连接(控制通道 / MCP)",
"ai.prefs.allow_control_note": "仅在本机开放控制通道(127.0.0.1,端口 27878 或下一个空闲端口)。连接的程序还必须出示一个每次启动时新生成、只有你的用户账户能读取的秘密令牌。下次启动 SolidCraft 时生效。关闭:除非用 --control 启动,否则不监听任何端口。",
"ai.prefs.availability": "助手控制",
"ai.prefs.background_commands": "在后台执行代理命令",
"ai.prefs.connect_apps": "让此电脑上的 AI 应用可以使用 SolidCraft",
"ai.prefs.connect_apps_note": "SolidCraft 启动时,会把自己作为 MCP 服务器添加到此电脑上找到的 AI 应用(OpenCode、Claude Desktop、Claude Code、Cursor、Codex、Gemini CLI、Copilot CLI、Windsurf、Zed、VS Code)。只添加或更新 solidcraft 条目;你修改过的条目不会被改动,文件会先备份。关闭后,会删除它添加的条目。",
"ai.prefs.follow_camera": "将相机转向草图",
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
@@ -663,6 +692,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": "由实体创建组件",
@@ -747,6 +777,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": "曲率梳分析",
@@ -756,6 +788,7 @@
"cmd.inspect_interference": "干涉",
"cmd.inspect_isocurve": "等参线分析",
"cmd.inspect_minimum_radius": "最小半径分析",
"cmd.inspect_parameter_sweep": "参数扫描",
"cmd.inspect_silhouette": "轮廓",
"cmd.inspect_zebra": "斑马纹分析",
"cmd.joint_as_built": "现状接头",
@@ -881,6 +914,7 @@
"cmd.sheet_fold": "折叠",
"cmd.sheet_hem": "卷边",
"cmd.sheet_manage_rules": "钣金规则",
"cmd.sheet_path_flange": "路径法兰",
"cmd.sheet_perforate": "冲孔",
"cmd.sheet_refold": "重新折弯",
"cmd.sheet_rules": "列出钣金规则",
@@ -1058,6 +1092,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": "计算",
@@ -1073,7 +1108,18 @@
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
"dialog.selection_step": "第 {step} 步,共 {total} 步:选择{input}",
"dialog.step": "步骤",
"document.changed_on_disk": "{name} 已在磁盘上更改",
"document.changed_on_disk_detail": "自您打开或上次保存 {name} 以来,它已被其他程序或计算机更改。覆盖会用您的更改替换这些更改;保存副本则两者都保留。",
"document.copy_name": "{name} 副本",
"document.open_anyway": "仍然打开",
"document.open_elsewhere": "{name} 已被打开",
"document.open_elsewhere_detail": "{name} 已在 {who} 上的 SolidCraft {app} 中打开({when}打开)。如果双方都保存,其中一方的工作会丢失。以只读方式打开即可查看而不会覆盖它。",
"document.open_read_only": "以只读方式打开",
"document.opened_read_only": "已以只读方式打开:保存时会要求新文件名",
"document.overwrite": "覆盖",
"document.read_only_save": "此设计以只读方式打开:请另存为新文件",
"document.save_changes": "保存对 {name} 的更改?",
"document.save_copy": "保存副本…",
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
"drive.stopped_by_contact": "停在 {value}:{a} 会穿过 {b}",
"drive.stopped_by_loop": "停在 {value}:经过 {joint} 的环路无法再闭合",
@@ -1090,9 +1136,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": "正在准备视图...",
@@ -1102,6 +1150,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": "取自型材",
@@ -1109,17 +1158,25 @@
"help.apache": "Apache 许可证 2.0",
"help.attributions": "署名信息 (ATTRIBUTION.md)",
"help.build": "版本 {version} · 提交 {commit} · 构建日期 {date}",
"help.connect_note": "请重启各个助手以加载 SolidCraft 服务器。每个被修改的文件都已备份为 .bak。",
"help.connect_title": "连接 AI 助手",
"help.font_details": "界面使用 egui 的内置字体(Ubuntu Light 和 Hack,采用 Ubuntu Font Licence 和 MIT/Bitstream Vera 许可证;Noto Emoji 和表情图标字体,采用 OFL 和 MIT 许可证)。草图文本使用 Liberation Sans(SIL OFL 1.1)。The Crafting Apps 的可选字体包 craft-fonts(BIZ UDPGothic、Shippori Mincho、BIZ UDMincho、Noto Sans CJK SC、Noto Sans Arabic;SIL OFL 1.1)未随 SolidCraft 构建版本捆绑提供。",
"help.fonts": "字体",
"help.licences": "许可证",
"help.mit": "MIT 许可证",
"help.notice": "NOTICE",
"help.solid.chamfer": "用指定的距离在所选边上切出一个平直的斜面。用于去除锐边,或使销钉、螺钉更容易插入。",
"help.solid.combine": "将多个实体合并、从目标实体中切除,或只保留它们共有的体积,还可以保留工具实体。用于合并零件、用一个形体切割另一个形体,或找出两个实体重叠的位置。",
"help.solid.extrude": "将草图轮廓按指定距离拉伸为三维形体,可以增加或去除材料。用于把平面轮廓变成方块、凸台或凹槽。",
"help.solid.fillet": "用指定半径的平滑圆弧将所选的边倒圆。用于出于强度、手感或外观的考虑而柔化锐边。",
"help.solid.hole": "在面上的指定位置钻出指定直径的圆孔。用于螺钉过孔、销孔等简单的圆形开口。",
"help.solid.loft": "在两个或更多草图轮廓之间,或从轮廓到一个点,生成平滑过渡的实体。用于过渡段、漏斗、手柄等截面沿长度变化的形体。",
"help.solid.mirror": "将所选实体或特征相对于一个平面生成镜像副本,还可以与原件合并。用于对称零件,只需建一半即可得到两侧。",
"help.solid.pattern.circular": "将所选对象按指定的数量和总角度绕轴重复排列。用于螺栓圆周分布、辐条、散热片和轮齿。",
"help.solid.pattern.rectangular": "将所选对象按指定的数量和间距沿一个或两个方向重复排列。用于成排或成网格的孔、加强筋、销钉或支脚。",
"help.solid.revolve": "将草图轮廓绕一条轴旋转,生成圆形的实体。用于轴、旋钮、碗等围绕中心线呈圆形的零件。",
"help.solid.shell": "将实体掏空,留下指定厚度的壁,还可以打开所选的面。用于盒子、盖子等薄壁零件。",
"help.solid.sweep": "沿路径拖动草图轮廓生成实体。用于管道、导轨、手柄等截面恒定且沿曲线延伸的零件。",
"help.third_party": "第三方许可证",
"home.back": "返回 {name}",
"home.crashes": "崩溃报告",
@@ -1196,6 +1253,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}",
@@ -1301,10 +1360,22 @@
"pattern.direction_1": "方向 1",
"pattern.direction_2": "方向 2",
"pattern.flip_direction": "反转方向",
"perf.click_for_details": "点击查看详情:帧耗时图、直方图和 GPU 绘制的内容",
"perf.draw_calls": "绘制调用",
"perf.line_segments": "线段",
"perf.low_1": "1% 最低",
"perf.renderer": "渲染器",
"perf.scene_build": "场景构建",
"perf.triangles": "三角形",
"perf.ui_time": "界面耗时",
"perf.viewport": "视图",
"perf.worst": "最差",
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
"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": "助手",
@@ -1348,6 +1419,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": "历史",
@@ -1379,6 +1454,7 @@
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
"prefs.orientation": "方向",
"prefs.overall_speed": "整体速度",
"prefs.prefer_free_sketch_points": "草图点重合时优先选择可移动的点",
"prefs.preview_choices": "所选设置预览",
"prefs.quality_default": "默认",
"prefs.quality_fastest": "最快",
@@ -2052,7 +2128,9 @@
"search.try": "输入命令名称、此设计中的特征或参数。要带值运行命令,请输入其ID和JSON对象:{example}",
"search.valid": "有效",
"section.off_hint": "未选择平面:按“确定”将移除剖面。",
"set by the sketch's lines": "由草图的线条决定",
"settings.display_quality_note": "仅影响显示,不影响设计和导出。",
"settings.frame_rate_details_note": "添加最近各帧耗时的图表(叠加绘制的三角形数)、帧耗时直方图以及 GPU 绘制的内容。点击视图中的帧率可显示或隐藏。",
"settings.frame_rate_note": "在视图右下角显示每秒帧数。",
"settings.measure_axis_distances": "显示 X/Y/Z 距离",
"settings.measure_axis_distances_help": "测量对话框打开时,在总距离之外显示沿世界坐标轴方向的尺寸线和距离。",
@@ -2083,6 +2161,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": "编辑文字及其位置,然后按“确定”。",
@@ -2107,6 +2190,7 @@
"timeline.delete_after_marker_one": "删除历史标记之后的 1 个特征",
"timeline.delete_after_marker_other": "删除历史标记之后的 {count} 个特征",
"timeline.delete_many": "删除所选的 {count} 个特征",
"timeline.feature_hint": "双击编辑 · 拖动以重新排序 · 右键查看更多操作",
"timeline.group": "新建特征文件夹",
"timeline.move_many": "移动时间线项目",
"timeline.move_to_group": "将特征移至文件夹",
@@ -2254,7 +2338,9 @@
"ui.agent_chat_auth_title": "登录智能体",
"ui.agent_chat_cancel": "取消",
"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": "连接",
@@ -2305,6 +2391,7 @@
"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": "开始新对话?当前对话将被清除。",
@@ -2313,6 +2400,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": "未安装",
@@ -2330,6 +2418,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": "已排队",
@@ -2358,6 +2449,7 @@
"ui.agent_chat_stop_tip": "停止智能体及其启动的所有进程",
"ui.agent_chat_stop_turn_tip": "停止智能体(Esc)。排队的消息仍保持排队。",
"ui.agent_chat_turn_limit": "每条消息的更改数",
"ui.agent_chat_turn_unlimited": "不限",
"ui.agent_chat_undo_changes": "撤销这些更改",
"ui.agent_chat_undo_label": "智能体聊天:{message}",
"ui.agent_chat_undone": "(已撤销)",
@@ -2426,7 +2518,9 @@
"ui.ai_preset": "预设",
"ui.ai_protocol": "协议",
"ui.ai_remove": "移除",
"ui.ai_request_limit": "每条消息 24 次请求后已停止;再发送一条消息即可继续",
"ui.ai_request_timed_out": "模型用时超过 {seconds} 秒。可在“首选项 › 助手”中调高此端点的请求超时。",
"ui.ai_request_timeout": "请求超时",
"ui.ai_request_timeout_hint": "对此端点的一次模型请求最多可用的时间,30 秒到 1 小时。本地模型较慢时请调高。",
"ui.ai_reset": "重置",
"ui.ai_settings": "AI 设置…",
"ui.ai_show_features": "显示 AI 功能",
@@ -2534,6 +2628,8 @@
"ui.copy": "复制",
"ui.corner": "角",
"ui.corners": "角点",
"ui.cosmetic_thread_markings": "装饰螺纹标记",
"ui.cosmetic_thread_markings_help": "在带螺纹的圆柱面上显示示意标记。仅改变显示,不影响设计或导出。",
"ui.counterbore": "沉头孔",
"ui.countersink": "锥形沉孔",
"ui.create": "创建",
@@ -2610,6 +2706,8 @@
"ui.filter_commands_or_keys": "筛选命令或按键",
"ui.findInTimeline": "在时间线中查找",
"ui.findInWindow": "在窗口中查找",
"ui.fine": "精细",
"ui.finest": "最精细",
"ui.finish_sketch": "完成草图",
"ui.fit": "适合",
"ui.fix_unfix": "固定/取消固定",
@@ -2617,6 +2715,7 @@
"ui.flip": "翻转",
"ui.format": "格式",
"ui.frame_rate": "帧率",
"ui.frame_rate_details": "帧率详情",
"ui.front": "前视",
"ui.fully_constrained": "完全约束",
"ui.function": "函数",
@@ -2802,6 +2901,7 @@
"ui.one_side": "单侧",
"ui.open": "打开",
"ui.open_ends_tip": "未接触任何曲线",
"ui.open_linked_design": "打开链接设计",
"ui.open_preferences": "打开首选项",
"ui.open_recent": "打开最近",
"ui.operation": "操作",
@@ -2821,6 +2921,11 @@
"ui.paste": "粘贴",
"ui.path": "路径",
"ui.pattern_on_path": "路径阵列",
"ui.perforate_margin_default": "与直径相同",
"ui.perforate_pitch": "孔距",
"ui.perforate_square": "正方形排列",
"ui.perforate_staggered": "交错排列(60°)",
"ui.perforate_title": "穿孔",
"ui.perpendicular": "垂直",
"ui.perspective_view": "透视图",
"ui.pinToShortcuts": "固定到快捷方式",
@@ -2832,7 +2937,9 @@
"ui.plugins": "插件",
"ui.plugins_desktop": "插件仅在桌面版中可用。",
"ui.plugins_empty": "尚未安装插件。",
"ui.plugins_none": "没有符合搜索的插件。",
"ui.plugins_note": "每个插件是 {path} 里的一个文件夹,其中有 plugin.json。SolidCraft 只读取该文件,不运行文件夹里的代码。关闭插件仍会保留安装。",
"ui.plugins_search": "搜索插件",
"ui.plugins_skipped": "有 {count} 个文件夹无法读取。",
"ui.plugins_version": "{version}",
"ui.point": "点",
@@ -2848,6 +2955,7 @@
"ui.project_face_edges_sketch": "将面的边投影到新草图",
"ui.properties": "属性",
"ui.pull_direction": "拔模方向",
"ui.quality": "质量",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "退出",
@@ -2954,9 +3062,12 @@
"ui.spline": "样条曲线",
"ui.split_body": "分割实体",
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
"ui.standard": "标准",
"ui.start": "开始",
"ui.start_over": "重新开始",
"ui.start_page": "起始页",
"ui.step_values": "使用方向键和滚轮调整数值",
"ui.step_values_tip": "在正在输入的数值框中,上下方向键和框上的鼠标滚轮可将数字增减 1(Shift:10,Alt:0.1)。表达式保持不变。",
"ui.stretch": "伸展",
"ui.supersampling": "超级采样 (SSAA)",
"ui.supersampling_adaptive": "仅在视图静止时",
@@ -2991,6 +3102,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": "双侧",
@@ -3008,6 +3120,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": "视觉样式",
@@ -3061,6 +3174,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": "稍后",
@@ -3071,6 +3185,11 @@
"update.up_to_date": "SolidCraft 已是最新版本。",
"update.update_now": "立即更新",
"update.you_have": "当前版本为 {version}。",
"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 关闭"
}
+4 -1
View File
@@ -8,7 +8,7 @@
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolidCraft by
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli, the licence files (including
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli (and adds its folder to the system PATH), the licence files (including
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.solidcraft` (and of the pre-rename `.solvecraft`), and offers it under
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
@@ -122,6 +122,9 @@
<Component Id="SolidcraftCli">
<File Id="SolidcraftCliExe" Source="$(var.BinDir)\solidcraft-cli.exe" KeyPath="yes" />
<!-- The install folder on the system PATH, so MCP clients and shells find `solidcraft-cli` by name;
removed again on uninstall. -->
<Environment Id="SolidcraftPath" Name="PATH" Value="[INSTALLFOLDER]" Action="set" Part="last" System="yes" Permanent="no" />
</Component>
<!-- Compatibility shim (docs/compatibility.md): the old CLI name, forwarding to solidcraft-cli. -->
<Component Id="SolidcraftCliAlias">
+82 -8
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);
@@ -482,8 +491,62 @@ struct LOut {
@location(1) wp: vec3<f32>,
};
/// A normal from its 12-bit octahedral form (`octahedral6` in view.rs); 0 is unknown (zero).
fn octahedral(q: u32) -> vec3<f32> {
let qx = q & 63u;
let qy = (q >> 6u) & 63u;
if (qx == 0u || qy == 0u) { return vec3<f32>(0.0); }
var x = f32(qx - 1u) / 31.0 - 1.0;
var y = f32(qy - 1u) / 31.0 - 1.0;
let z = 1.0 - abs(x) - abs(y);
if (z < 0.0) {
let ox = x;
let oy = y;
x = (1.0 - abs(oy)) * select(-1.0, 1.0, ox >= 0.0);
y = (1.0 - abs(ox)) * select(-1.0, 1.0, oy >= 0.0);
}
return normalize(vec3<f32>(x, y, z));
}
/// How steeply the faces beside an edge are seen at `p`: the depth a face gains per pixel across
/// the edge, in pixels (tan of the angle between its normal and the view), the larger of the two;
/// 1 when neither is known. Capped, so a face seen exactly edge on does not lift the band far.
fn grazing(p: vec3<f32>, na: vec3<f32>, nb: vec3<f32>) -> f32 {
var toward = normalize(u.back.xyz);
if (u.eye.w > 0.5) { toward = normalize(u.eye.xyz - p); }
var g = 1.0;
for (var i = 0; i < 2; i++) {
let n = select(nb, na, i == 0);
if (dot(n, n) < 0.25) { continue; }
let d = abs(dot(n, toward));
g = max(g, sqrt(max(1.0 - d * d, 0.0)) / max(d, 0.02));
}
return min(g, 50.0);
}
/// Clip z of the world point `p` (clip position `c`) moved `px` pixels toward the eye: the front
/// of a thin cylinder around an edge, so the band drawn along it is not buried by a face next to
/// the edge seen at a grazing angle. Measured on the screen, it is the same at every zoom.
fn lifted_z(p: vec3<f32>, c: vec4<f32>, px: f32, half: vec2<f32>) -> f32 {
let b = normalize(u.back.xyz);
var a = vec3<f32>(0.0, 0.0, 1.0);
if (abs(b.z) > 0.9) { a = vec3<f32>(1.0, 0.0, 0.0); }
let side = normalize(cross(b, a));
let cs = u.vp * vec4<f32>(p + side, 1.0);
if (cs.w <= 1e-6) { return c.z; }
// Pixels per millimetre across the view at the point's depth.
let ppm = length((cs.xy / cs.w - c.xy / c.w) * half);
if (ppm < 1e-9) { return c.z; }
var toward = b;
if (u.eye.w > 0.5) { toward = normalize(u.eye.xyz - p); }
let q = u.vp * vec4<f32>(p + toward * (px / ppm), 1.0);
if (q.w <= 1e-6) { return c.z; }
// The same depth at the original w, so the band stays where it was on the screen.
return q.z / q.w * c.w;
}
@vertex
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) wbits: u32) -> LOut {
var pa = a;
var pb = b;
var ca = u.vp * vec4<f32>(a, 1.0);
@@ -518,11 +581,22 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
// A line thinner than a pixel is drawn a pixel wide, faded by the width it lacks: a narrower
// quad covers no pixel centre when it falls between two rows, so without multisampling thin
// lines (the grid's) came and went as the view zoomed (#2823).
let wd = max(w, 1.0);
// The top bit marks a lifted highlight (`GpuScene::lifted_line`, `lifted_width`); a plain
// line's slot is its width as an f32.
let lifted = (wbits & 0x80000000u) != 0u;
var wa = bitcast<f32>(wbits);
if (lifted) { wa = f32((wbits >> 24u) & 127u) / 8.0; }
let wd = max(wa, 1.0);
let off = nrm * s * wd * 0.5 + dir * (t * 2.0 - 1.0) * wd * 0.5;
var z = base.z;
if (lifted && base.w > 1e-6) {
let p = select(pa, pb, t > 0.5);
let g = grazing(p, octahedral(wbits & 4095u), octahedral((wbits >> 12u) & 4095u));
z = lifted_z(p, base, wd * 0.5 * g + 1.0, half);
}
var o: LOut;
o.pos = vec4<f32>(base.xy + off / half * base.w, base.z, base.w);
o.c = vec4<f32>(c.rgb, c.a * (w / wd));
o.pos = vec4<f32>(base.xy + off / half * base.w, z, base.w);
o.c = vec4<f32>(c.rgb, c.a * (wa / wd));
o.wp = select(pa, pb, t > 0.5);
return o;
}
+9 -8
View File
@@ -155,9 +155,11 @@ impl Tokens {
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
origin_point: Color32::from_rgb(214, 215, 218),
axis_x: Color32::from_rgb(255, 0, 0),
axis_y: Color32::from_rgb(0, 255, 0),
axis_z: Color32::from_rgb(2, 2, 248),
// The one set of X, Y and Z colours (#976), equal to the engine's `view::colors::AXIS_*`
// so a headless picture draws its origin axes as the window does.
axis_x: Color32::from_rgb(220, 60, 60),
axis_y: Color32::from_rgb(60, 170, 60),
axis_z: Color32::from_rgb(60, 100, 230),
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
construction_geom: icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[0]),
body: Color32::from_rgb(176, 186, 198),
@@ -178,6 +180,7 @@ impl Tokens {
body_selected: Color32::from_rgb(110, 170, 235),
sketch: Color32::from_rgb(30, 90, 200),
sketch_fixed: Color32::from_rgb(20, 20, 20),
sketch_halo: Color32::from_rgb(240, 243, 247),
sketch_point: Color32::BLACK,
sketch_locked: Color32::from_rgb(0, 140, 60),
dimension: Color32::from_rgb(36, 40, 48),
@@ -219,8 +222,6 @@ impl Tokens {
sketch_lost: Color32::from_rgb(200, 60, 90),
sketch_construction: Color32::from_rgb(230, 130, 40),
sketch_centerline: Color32::from_rgb(200, 110, 60),
triad: [Color32::from_rgb(220, 60, 60), Color32::from_rgb(60, 170, 60), Color32::from_rgb(60, 100, 230)],
gizmo: [Color32::from_rgb(225, 70, 60), Color32::from_rgb(80, 190, 80), Color32::from_rgb(70, 130, 240)],
bend_up: Color32::from_rgb(30, 140, 50),
bend_down: Color32::from_rgb(200, 90, 20),
interference: Color32::from_rgb(235, 45, 40),
@@ -336,6 +337,7 @@ impl Tokens {
t.body_edge = Color32::from_rgb(16, 16, 18);
t.sketch = Color32::from_rgb(92, 150, 255);
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
t.sketch_halo = Color32::from_rgb(28, 32, 40);
t.sketch_point = Color32::from_rgb(232, 234, 238);
t.sketch_locked = Color32::from_rgb(96, 204, 116);
t.dimension = Color32::from_rgb(224, 228, 234);
@@ -375,9 +377,8 @@ impl Tokens {
let mut t = self;
let positive = [0.20, 0.45, 0.90];
let negative = [0.85, 0.40, 0.05];
t.triad[0] = Color32::from_rgb(200, 40, 40);
t.triad[1] = Color32::from_rgb(120, 210, 80);
t.gizmo[1] = Color32::from_rgb(140, 220, 90);
t.axis_x = Color32::from_rgb(200, 40, 40);
t.axis_y = Color32::from_rgb(120, 210, 80);
t.preview_sketch = Color32::from_rgb(80, 185, 145);
t.analysis.draft = [positive, [0.95, 0.90, 0.60], negative];
t.analysis.access = [positive, negative];
+245
View File
@@ -0,0 +1,245 @@
{
"base": "light",
"colors": {
"accent": "#c63a00",
"accent_soft": "#e8d0c3",
"agent_busy": "#015ba6",
"agent_ok": "#257231",
"app_bar": "#e1ddd8",
"app_bar_hover": "#13110e14",
"app_bar_hover_button": "#13110e18",
"app_bar_hover_strong": "#13110e22",
"app_bar_hover_subtle": "#13110e0c",
"app_bar_icon": [
"#403d38",
"#b5b0a9",
"#c63a00"
],
"app_bar_separator": "#b5b0a9",
"app_bar_tab_active": "#f9f6f2",
"app_bar_text": "#403d38",
"axis_x": "#c71529",
"axis_y": "#257231",
"axis_z": "#015ba6",
"bend_down": "#705e91",
"bend_up": "#257231",
"body": "#c2beb8",
"body_edge": "#403d38",
"body_selected": "#de8d6b",
"border": "#d9d5d0",
"box_crossing": "#257231",
"box_window": "#015ba6",
"chip": "#c63a00",
"construction_geom": "#7d6535",
"construction_plane": "#7d653540",
"continuity": [
"#257231",
"#015ba6",
"#c08a1a",
"#b14578"
],
"control_border": "#d4d0ca",
"control_fill": "#fffdfa",
"cube_border": "#b5b0a9",
"cube_face": "#fffdfa",
"cube_hover": "#d4d0ca",
"cube_text": "#13110e",
"dialog_bg": "#f9f6f2",
"dialog_body": "#f9f6f2",
"dialog_border": "#d4d0ca",
"dialog_footer": "#efece8",
"dialog_label": "#403d38",
"dialog_rule": "#d9d5d0",
"dimension": "#403d38",
"dimension_driven": "#6c6863",
"error": "#c71529",
"field": "#fffdfa",
"form_accent": "#5844bc",
"form_dim": "#6c6863",
"form_error": "#c71529",
"form_error_fill": "#f4e0de",
"form_fill": "#5844bc",
"form_ink": "#13110e",
"form_primary": "#c63a00",
"form_soft": "#e4e0e5",
"form_success": "#257231",
"form_success_fill": "#e4e9df",
"form_tab_ink": "#5844bc",
"gizmo": [
"#b00018",
"#096125",
"#004c9b"
],
"glyph_bg": "#fffdfa",
"glyph_edge": "#d4d0ca",
"glyph_text": "#604b83",
"grid": "#edeae5",
"grid_major": "#e1ddd8",
"heading_bg": "#ebe7e2",
"hover": "#e1ddd7",
"hover_edge": "#13110e",
"hover_edge_halo": "#fffdfa",
"hover_profile": "#13110e22",
"hover_profile_edge": "#403d38",
"icon": "#403d38",
"icon_fill": "#d4d0ca",
"icons.access": [
"#257231",
"#c71529"
],
"icons.agent": "#5844bc",
"icons.axes": [
"#c71529",
"#257231",
"#015ba6"
],
"icons.curvature": [
"#015ba6",
"#257231",
"#c71529"
],
"icons.draft": [
"#257231",
"#e6af2b"
],
"icons.floor": "#6c6863",
"icons.fold": "#8d8982",
"icons.folder": "#c9a96a",
"icons.glint": "#ffffffa0",
"icons.highlight": "#fffdfa",
"icons.interference": "#c71529",
"icons.marker_halo": "#13110e96",
"icons.paint": [
"#c71529",
"#015ba6",
"#e6af2b"
],
"icons.plane": "#e3cf9f",
"icons.sheet": "#b9cde2",
"icons.sheet_2": "#a5bfd9",
"icons.sheet_light": "#dbe6f0",
"icons.sheet_line": "#015ba6",
"icons.sky": "#b9cde2",
"icons.stripe": "#13110e",
"icons.warning": "#e6af2b",
"interference": "#c71529",
"logo": [
"#bb461e",
"#c63a00",
"#902e04"
],
"manipulator": "#004c9b",
"mesh_accent": "#7e6900",
"mesh_fill": "#7e6900",
"mesh_ink": "#6b5903",
"mesh_soft": "#e7e3d8",
"on_accent": "#fffbed",
"origin_plane": "#6c686338",
"origin_plane_edge": "#6c6863c8",
"origin_plane_hover": "#015ba670",
"origin_point": "#403d38",
"overlay": "#fffdfaeb",
"panel": "#efece8",
"panel_header": "#ebe7e2",
"preview_add": "#4f8fd6",
"preview_cut": "#c715296e",
"preview_new": "#e6af2b",
"preview_sketch": "#2d713b",
"profile_fill": "#015ba614",
"render_accent": "#ac1f6a",
"render_fill": "#ac1f6a",
"render_ink": "#ac1f6a",
"render_soft": "#eadedf",
"rubber_band": "#004c9b",
"section_cap": "#7d6535",
"sel_edge": "#c63a00",
"sel_edge_rim": "#e29c80",
"sel_face": "#d67147",
"sel_profile": "#c63a00a0",
"sel_profile_edge": "#c63a00",
"sel_vertex": "#c63a00",
"selected_bg": "#f1d8cb",
"selected_ink": "#13110e",
"sheet_fill": "#015ba6",
"sheet_ink": "#015ba6",
"sheet_soft": "#dee2e3",
"sketch": "#015ba6",
"sketch_accent": "#2d713b",
"sketch_centerline": "#403d38",
"sketch_construction": "#7d6535",
"sketch_fixed": "#13110e",
"sketch_ink": "#2d713b",
"sketch_locked": "#257231",
"sketch_lost": "#c71529",
"sketch_point": "#13110e",
"sketch_point_free": "#015ba6",
"sketch_projected": "#705e91",
"sketch_soft": "#e1e3dc",
"subheading_bg": "#efece8",
"surface_accent": "#7d6535",
"surface_fill": "#7d6535",
"surface_ink": "#6a562e",
"surface_soft": "#e7e3db",
"tab_on_fill_dark": "#13110e",
"text": "#13110e",
"text_dim": "#6c6863",
"timeline": "#efece8",
"timeline_item": "#fffdfa",
"timeline_item_error": "#f2c5c8",
"timeline_marker": "#c63a00",
"toolbar": "#efece8",
"triad": [
"#c71529",
"#257231",
"#015ba6"
],
"viewport_bottom": "#f1ede8",
"viewport_top": "#f9f6f2",
"warning": "#956000"
},
"format": "solidcraft-theme/1",
"name": "Akari",
"icon_palette": {
"ink": "#13110e",
"line": "#505054",
"passive-1": "#dee5ee",
"passive-2": "#aeb5bd",
"passive-3": "#81878f",
"guide": "#6e6e73",
"solid": [
"#dbe9fe",
"#81b3fa",
"#1875eb"
],
"surface": [
"#fde3c4",
"#daa662",
"#a06f28"
],
"mesh": [
"#fee7a2",
"#d1ad37",
"#937717"
],
"form": [
"#ede2ff",
"#c39afc",
"#8b62bf"
],
"sketch": [
"#b1fdc5",
"#72c78b",
"#388e56"
],
"constraint": [
"#fee0dd",
"#f98e87",
"#d73339"
],
"construction": [
"#fde3c4",
"#daa662",
"#a06f28"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#d4902f",
"form_success_fill": "#3b280d",
"form_tab_ink": "#cf8d31",
"gizmo": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"glyph_bg": "#201607",
"glyph_edge": "#e0d1ac",
"glyph_text": "#e0d1ac",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ba7e29",
"sketch_construction": "#ad7626",
"sketch_fixed": "#c68b2f",
"sketch_halo": "#170d03",
"sketch_ink": "#f7ce88",
"sketch_locked": "#d4902f",
"sketch_lost": "#fff0ca",
@@ -188,11 +184,6 @@
"timeline_item_error": "#46300f",
"timeline_marker": "#9b6922",
"toolbar": "#100b04",
"triad": [
"#f3a535",
"#ffad38",
"#9b6922"
],
"viewport_bottom": "#090603",
"viewport_top": "#090603",
"warning": "#ffd274"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#6ee0bb",
"form_success_fill": "#153e47",
"form_tab_ink": "#b6a9e4",
"gizmo": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"glyph_bg": "#0a233e",
"glyph_edge": "#ff968d",
"glyph_text": "#ff968d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e7b374",
"sketch_construction": "#78b8dc",
"sketch_fixed": "#e3f0fa",
"sketch_halo": "#092c50",
"sketch_ink": "#9ddab7",
"sketch_locked": "#6ee0bb",
"sketch_lost": "#ff968d",
@@ -188,11 +184,6 @@
"timeline_item_error": "#422d40",
"timeline_marker": "#94b2cb",
"toolbar": "#0c223c",
"triad": [
"#ec9e99",
"#9ddab7",
"#91c8ff"
],
"viewport_bottom": "#143b67",
"viewport_top": "#143b67",
"warning": "#f0d18c"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a6e3a1",
"form_success_fill": "#353f42",
"form_tab_ink": "#b4befe",
"gizmo": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"glyph_bg": "#313244",
"glyph_edge": "#fc93b0",
"glyph_text": "#fc93b0",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f9e2af",
"sketch_construction": "#f9e2af",
"sketch_fixed": "#cdd6f4",
"sketch_halo": "#11111b",
"sketch_ink": "#a6e3a1",
"sketch_locked": "#a6e3a1",
"sketch_lost": "#f38ba8",
@@ -188,11 +184,6 @@
"timeline_item_error": "#51384b",
"timeline_marker": "#a6adc8",
"toolbar": "#1e1e2e",
"triad": [
"#f38ba8",
"#a6e3a1",
"#477dc8"
],
"viewport_bottom": "#11111b",
"viewport_top": "#11111b",
"warning": "#f9e2af"
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#b4b4b8",
"app_bar_tab_active": "#f4f4f4",
"app_bar_text": "#242426",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#e95049",
"axis_y": "#35a754",
"axis_z": "#297fdb",
"bend_down": "#bd843f",
"bend_up": "#36934d",
"body": "#b6bbc3",
@@ -65,11 +65,6 @@
"form_success": "#237b38",
"form_success_fill": "#e6f3e8",
"form_tab_ink": "#75559f",
"gizmo": [
"#ed514b",
"#36ad59",
"#2184e9"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#d73339",
"glyph_text": "#d73339",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b77c47",
"sketch_construction": "#cb8b37",
"sketch_fixed": "#262628",
"sketch_halo": "#ffffff",
"sketch_ink": "#236f40",
"sketch_locked": "#318348",
"sketch_lost": "#c94f68",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f8dedb",
"timeline_marker": "#63636b",
"toolbar": "#ececec",
"triad": [
"#e95049",
"#35a754",
"#297fdb"
],
"viewport_bottom": "#d9dce2",
"viewport_top": "#f5f5f7",
"warning": "#ad7400"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#83c69b",
"form_success_fill": "#35423e",
"form_tab_ink": "#a796cb",
"gizmo": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"glyph_bg": "#303338",
"glyph_edge": "#e57071",
"glyph_text": "#e57071",
@@ -168,6 +163,7 @@
"sketch_centerline": "#dbb879",
"sketch_construction": "#dbb879",
"sketch_fixed": "#e0e2e5",
"sketch_halo": "#17191d",
"sketch_ink": "#88b298",
"sketch_locked": "#83c69b",
"sketch_lost": "#ed7777",
@@ -188,11 +184,6 @@
"timeline_item_error": "#553a3d",
"timeline_marker": "#a0a6b0",
"toolbar": "#25272b",
"triad": [
"#ed7777",
"#83c69b",
"#2878a9"
],
"viewport_bottom": "#17191d",
"viewport_top": "#17191d",
"warning": "#dbb879"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#39d45c",
"form_success_fill": "#133b20",
"form_tab_ink": "#36b95c",
"gizmo": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"glyph_bg": "#09200e",
"glyph_edge": "#afd5b6",
"glyph_text": "#afd5b6",
@@ -168,6 +163,7 @@
"sketch_centerline": "#32ba50",
"sketch_construction": "#2fad4a",
"sketch_fixed": "#8def9f",
"sketch_halo": "#021007",
"sketch_ink": "#bbffca",
"sketch_locked": "#39d45c",
"sketch_lost": "#d8ffdf",
@@ -188,11 +184,6 @@
"timeline_item_error": "#21462a",
"timeline_marker": "#5b9b6a",
"toolbar": "#061009",
"triad": [
"#c1f3ba",
"#4dff79",
"#599b70"
],
"viewport_bottom": "#010703",
"viewport_top": "#010703",
"warning": "#d1ffad"
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#141617",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +84,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#382c20",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#4e5954",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#4a3a2c",
+2 -10
View File
@@ -12,6 +12,7 @@
"text_dim": "#a89984",
"accent": "#83a598",
"accent_soft": "#48544f",
"selected_ink": "#ebdbb2",
"sketch_accent": "#a1a322",
"error": "#fb4934",
"warning": "#fabd2f",
@@ -53,6 +54,7 @@
"body_selected": "#83a598",
"sketch": "#83a598",
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
@@ -83,16 +85,6 @@
"bend_up": "#b8bb26",
"bend_down": "#fe8019",
"interference": "#fb4934",
"triad": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"gizmo": [
"#fb4934",
"#b8bb26",
"#83a598"
],
"icons.agent": "#d3869b",
"surface_fill": "#fe9755",
"surface_soft": "#423326",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fbf8e3",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f3e6c6",
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#f6edd0",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#edd7ad",
+1 -10
View File
@@ -53,6 +53,7 @@
"body_selected": "#83a598",
"sketch": "#076678",
"sketch_fixed": "#282828",
"sketch_halo": "#fcf5d8",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
@@ -83,16 +84,6 @@
"bend_up": "#79740e",
"bend_down": "#af3a03",
"interference": "#cc241d",
"triad": [
"#cc241d",
"#98971a",
"#458588"
],
"gizmo": [
"#cc241d",
"#98971a",
"#458588"
],
"icons.agent": "#8f3f71",
"surface_fill": "#df663c",
"surface_soft": "#f5e2b7",
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#40e080",
"form_success_fill": "#0b2616",
"form_tab_ink": "#df8fff",
"gizmo": [
"#ff6666",
"#40e080",
"#0070d0"
],
"glyph_bg": "#151515",
"glyph_edge": "#fa8a86",
"glyph_text": "#fa8a86",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ffcc00",
"sketch_construction": "#ffcc00",
"sketch_fixed": "#ffffff",
"sketch_halo": "#000000",
"sketch_ink": "#00ff80",
"sketch_locked": "#40e080",
"sketch_lost": "#ff6666",
@@ -188,11 +184,6 @@
"timeline_item_error": "#3d1818",
"timeline_marker": "#cccccc",
"toolbar": "#000000",
"triad": [
"#ff6666",
"#40e080",
"#0070d0"
],
"viewport_bottom": "#000000",
"viewport_top": "#000000",
"warning": "#ffcc00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#a3be8c",
"form_success_fill": "#424b4d",
"form_tab_ink": "#c3a5bd",
"gizmo": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"glyph_bg": "#3b4252",
"glyph_edge": "#f9959c",
"glyph_text": "#f9959c",
@@ -168,6 +163,7 @@
"sketch_centerline": "#ebcb8b",
"sketch_construction": "#ebcb8b",
"sketch_fixed": "#eceff4",
"sketch_halo": "#242f40",
"sketch_ink": "#a3be8c",
"sketch_locked": "#a3be8c",
"sketch_lost": "#bf616a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#513f4a",
"timeline_marker": "#a4b0c2",
"toolbar": "#2e3440",
"triad": [
"#bf616a",
"#a3be8c",
"#5e81ac"
],
"viewport_bottom": "#242f40",
"viewport_top": "#242f40",
"warning": "#ebcb8b"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#50fa7b",
"form_success_fill": "#2f4d42",
"form_tab_ink": "#d6acff",
"gizmo": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"glyph_bg": "#343746",
"glyph_edge": "#fc827b",
"glyph_text": "#fc827b",
@@ -168,6 +163,7 @@
"sketch_centerline": "#f1fa8c",
"sketch_construction": "#f1fa8c",
"sketch_fixed": "#f8f8f2",
"sketch_halo": "#1d1e29",
"sketch_ink": "#50fa7b",
"sketch_locked": "#50fa7b",
"sketch_lost": "#ff5555",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#a1a4c4",
"toolbar": "#282a36",
"triad": [
"#ff5555",
"#50fa7b",
"#8456b8"
],
"viewport_bottom": "#1d1e29",
"viewport_top": "#1d1e29",
"warning": "#f1fa8c"
+245
View File
@@ -0,0 +1,245 @@
{
"base": "dark",
"colors": {
"accent": "#ff7c50",
"accent_soft": "#3a2723",
"agent_busy": "#6daff7",
"agent_ok": "#89cf8e",
"app_bar": "#0f1113",
"app_bar_hover": "#e1e3e514",
"app_bar_hover_button": "#e1e3e518",
"app_bar_hover_strong": "#e1e3e522",
"app_bar_hover_subtle": "#e1e3e50c",
"app_bar_icon": [
"#bbbec1",
"#585b5f",
"#ff7c50"
],
"app_bar_separator": "#474b4f",
"app_bar_tab_active": "#1b1e20",
"app_bar_text": "#bbbec1",
"axis_x": "#f85052",
"axis_y": "#89cf8e",
"axis_z": "#6daff7",
"bend_down": "#ab9acd",
"bend_up": "#89cf8e",
"body": "#74777b",
"body_edge": "#0f1113",
"body_selected": "#c56241",
"border": "#2f3235",
"box_crossing": "#89cf8e",
"box_window": "#6daff7",
"chip": "#ff7c50",
"construction_geom": "#ceb687",
"construction_plane": "#ceb68740",
"continuity": [
"#89cf8e",
"#6daff7",
"#e6af2b",
"#f38bb7"
],
"control_border": "#474b4f",
"control_fill": "#2c2e31",
"cube_border": "#585b5f",
"cube_face": "#2c2e31",
"cube_hover": "#474b4f",
"cube_text": "#e1e3e5",
"dialog_bg": "#222527",
"dialog_body": "#222527",
"dialog_border": "#383b3e",
"dialog_footer": "#1b1e20",
"dialog_label": "#bbbec1",
"dialog_rule": "#2f3235",
"dimension": "#bbbec1",
"dimension_driven": "#8f9296",
"error": "#f85052",
"field": "#2c2e31",
"form_accent": "#a6a2fc",
"form_dim": "#8f9296",
"form_error": "#ff6060",
"form_error_fill": "#37292b",
"form_fill": "#a6a2fc",
"form_ink": "#e1e3e5",
"form_primary": "#ff7c50",
"form_soft": "#2f3043",
"form_success": "#89cf8e",
"form_success_fill": "#2c3631",
"form_tab_ink": "#a6a2fc",
"gizmo": [
"#ff6060",
"#98dfa2",
"#7abfff"
],
"glyph_bg": "#222527",
"glyph_edge": "#474b4f",
"glyph_text": "#ab9acd",
"grid": "#27292c",
"grid_major": "#2c2e31",
"heading_bg": "#15171a",
"hover": "#383b3e",
"hover_edge": "#0f1113",
"hover_edge_halo": "#e1e3e5",
"hover_profile": "#e1e3e522",
"hover_profile_edge": "#bbbec1",
"icon": "#bbbec1",
"icon_fill": "#474b4f",
"icons.access": [
"#89cf8e",
"#f85052"
],
"icons.agent": "#aba3ff",
"icons.axes": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"icons.curvature": [
"#6daff7",
"#89cf8e",
"#f85052"
],
"icons.draft": [
"#89cf8e",
"#e6af2b"
],
"icons.floor": "#383b3e",
"icons.fold": "#707376",
"icons.folder": "#ceb687",
"icons.glint": "#ffffffa0",
"icons.highlight": "#e1e3e5",
"icons.interference": "#f85052",
"icons.marker_halo": "#0f111396",
"icons.paint": [
"#f85052",
"#6daff7",
"#e6af2b"
],
"icons.plane": "#dcc9a0",
"icons.sheet": "#a2bedb",
"icons.sheet_2": "#8fb0d2",
"icons.sheet_light": "#c9d9ea",
"icons.sheet_line": "#6daff7",
"icons.sky": "#a2bedb",
"icons.stripe": "#0f1113",
"icons.warning": "#e6af2b",
"interference": "#f85052",
"logo": [
"#ff9c7c",
"#ff7c50",
"#b75c3e"
],
"manipulator": "#7abfff",
"mesh_accent": "#cfc152",
"mesh_fill": "#cfc152",
"mesh_ink": "#cfc152",
"mesh_soft": "#363624",
"on_accent": "#0f1113",
"origin_plane": "#8f929638",
"origin_plane_edge": "#8f9296c8",
"origin_plane_hover": "#6daff770",
"origin_point": "#bbbec1",
"overlay": "#15171af0",
"panel": "#15171a",
"panel_header": "#222527",
"preview_add": "#4c78a7",
"preview_cut": "#f850526e",
"preview_new": "#e6af2b",
"preview_sketch": "#8eca96",
"profile_fill": "#6daff71c",
"render_accent": "#f38bb7",
"render_fill": "#f38bb7",
"render_ink": "#f38bb7",
"render_soft": "#3d2c36",
"rubber_band": "#7abfff",
"section_cap": "#ceb687",
"sel_edge": "#ff7c50",
"sel_edge_rim": "#ffbea8",
"sel_face": "#dd6d47",
"sel_profile": "#ff7c50b0",
"sel_profile_edge": "#ff7c50",
"sel_vertex": "#ff7c50",
"selected_bg": "#45332e",
"selected_ink": "#e1e3e5",
"sheet_fill": "#6daff7",
"sheet_ink": "#6daff7",
"sheet_soft": "#253242",
"sketch": "#6daff7",
"sketch_accent": "#8eca96",
"sketch_centerline": "#bbbec1",
"sketch_construction": "#ceb687",
"sketch_fixed": "#e1e3e5",
"sketch_ink": "#8eca96",
"sketch_locked": "#89cf8e",
"sketch_lost": "#f85052",
"sketch_point": "#e1e3e5",
"sketch_point_free": "#6daff7",
"sketch_projected": "#998cb3",
"sketch_soft": "#2b3730",
"subheading_bg": "#1b1e20",
"surface_accent": "#ceb687",
"surface_fill": "#ceb687",
"surface_ink": "#ceb687",
"surface_soft": "#36342e",
"tab_on_fill_dark": "#0f1113",
"text": "#e1e3e5",
"text_dim": "#8f9296",
"timeline": "#15171a",
"timeline_item": "#222527",
"timeline_item_error": "#552f31",
"timeline_marker": "#ff7c50",
"toolbar": "#15171a",
"triad": [
"#f85052",
"#89cf8e",
"#6daff7"
],
"viewport_bottom": "#1b1e20",
"viewport_top": "#222527",
"warning": "#e6af2b"
},
"format": "solidcraft-theme/1",
"name": "Okibi",
"icon_palette": {
"ink": "#15171a",
"line": "#e1e3e5",
"passive-1": "#bfc1c5",
"passive-2": "#939599",
"passive-3": "#6a6c6f",
"guide": "#a0a6b0",
"solid": [
"#a0d7fb",
"#65bdf3",
"#4aa3d8"
],
"surface": [
"#fcc28e",
"#dea572",
"#c38c59"
],
"mesh": [
"#e8cd7d",
"#cab060",
"#b19747"
],
"form": [
"#d5c4fd",
"#b9a7df",
"#a08fc4"
],
"sketch": [
"#b0dbc0",
"#94bea4",
"#7ba58b"
],
"constraint": [
"#febbb9",
"#fb8e8d",
"#e57071"
],
"construction": [
"#fcc28e",
"#dea572",
"#c38c59"
]
}
}
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#8fdb9a",
"form_success_fill": "#2c4a33",
"form_tab_ink": "#c97fdf",
"gizmo": [
"#e06c75",
"#98c379",
"#61afef"
],
"glyph_bg": "#282c34",
"glyph_edge": "#f57f87",
"glyph_text": "#f57f87",
@@ -168,6 +163,7 @@
"sketch_centerline": "#d19a66",
"sketch_construction": "#d19a66",
"sketch_fixed": "#abb2bf",
"sketch_halo": "#181a1f",
"sketch_ink": "#98c379",
"sketch_locked": "#98c379",
"sketch_lost": "#e06c75",
@@ -188,11 +184,6 @@
"timeline_item_error": "#5c343d",
"timeline_marker": "#abb2bf",
"toolbar": "#21252b",
"triad": [
"#e06c75",
"#98c379",
"#61afef"
],
"viewport_bottom": "#282c34",
"viewport_top": "#282c34",
"warning": "#e5c07b"
+4 -13
View File
@@ -18,9 +18,9 @@
"app_bar_separator": "#aaaaaa",
"app_bar_tab_active": "#e6e6e6",
"app_bar_text": "#222222",
"axis_x": "#ff0000",
"axis_y": "#00ff00",
"axis_z": "#0202f8",
"axis_x": "#dc3c3c",
"axis_y": "#3caa3c",
"axis_z": "#3c64e6",
"bend_down": "#c85a14",
"bend_up": "#1e8c32",
"body": "#adadb6",
@@ -65,11 +65,6 @@
"form_success": "#336633",
"form_success_fill": "#e2ecdf",
"form_tab_ink": "#6d4e7d",
"gizmo": [
"#e1463c",
"#50be50",
"#4682f0"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#ab2125",
"glyph_text": "#ab2125",
@@ -168,6 +163,7 @@
"sketch_centerline": "#c86e3c",
"sketch_construction": "#e68228",
"sketch_fixed": "#222222",
"sketch_halo": "#ffffff",
"sketch_ink": "#456045",
"sketch_locked": "#447744",
"sketch_lost": "#c83c5a",
@@ -188,11 +184,6 @@
"timeline_item_error": "#f2dddd",
"timeline_marker": "#666666",
"toolbar": "#dddddd",
"triad": [
"#dc3c3c",
"#3caa3c",
"#3c64e6"
],
"viewport_bottom": "#ffffff",
"viewport_top": "#ffffff",
"warning": "#997722"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#859900",
"form_success_fill": "#dcdbb1",
"form_tab_ink": "#565a9d",
"gizmo": [
"#dc322f",
"#859900",
"#346e88"
],
"glyph_bg": "#e3ddc9",
"glyph_edge": "#da2f2d",
"glyph_text": "#da2f2d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#b58900",
"sketch_construction": "#b58900",
"sketch_fixed": "#586e75",
"sketch_halo": "#fdf6e3",
"sketch_ink": "#5a6800",
"sketch_locked": "#859900",
"sketch_lost": "#dc322f",
@@ -188,11 +184,6 @@
"timeline_item_error": "#eabcad",
"timeline_marker": "#657b83",
"toolbar": "#eee8d5",
"triad": [
"#dc322f",
"#859900",
"#346e88"
],
"viewport_bottom": "#fdf6e3",
"viewport_top": "#fdf6e3",
"warning": "#b58900"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#008000",
"form_success_fill": "#c0dcc0",
"form_tab_ink": "#7d007d",
"gizmo": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"glyph_bg": "#ffffff",
"glyph_edge": "#800000",
"glyph_text": "#800000",
@@ -168,6 +163,7 @@
"sketch_centerline": "#808000",
"sketch_construction": "#808000",
"sketch_fixed": "#000000",
"sketch_halo": "#ffffff",
"sketch_ink": "#005400",
"sketch_locked": "#008000",
"sketch_lost": "#800080",
@@ -188,11 +184,6 @@
"timeline_item_error": "#ffffc0",
"timeline_marker": "#000000",
"toolbar": "#c0c0c0",
"triad": [
"#ff0000",
"#00ff00",
"#0000ff"
],
"viewport_bottom": "#008080",
"viewport_top": "#008080",
"warning": "#ffff00"
+1 -10
View File
@@ -65,11 +65,6 @@
"form_success": "#9ece6a",
"form_success_fill": "#303932",
"form_tab_ink": "#bb9af7",
"gizmo": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"glyph_bg": "#24283b",
"glyph_edge": "#f6758d",
"glyph_text": "#f6758d",
@@ -168,6 +163,7 @@
"sketch_centerline": "#e0af68",
"sketch_construction": "#e0af68",
"sketch_fixed": "#c0caf5",
"sketch_halo": "#151722",
"sketch_ink": "#9ece6a",
"sketch_locked": "#9ece6a",
"sketch_lost": "#f7768e",
@@ -188,11 +184,6 @@
"timeline_item_error": "#4f313f",
"timeline_marker": "#9aa5ce",
"toolbar": "#1a1b26",
"triad": [
"#f7768e",
"#9ece6a",
"#4775ce"
],
"viewport_bottom": "#151722",
"viewport_top": "#151722",
"warning": "#e0af68"
+12
View File
@@ -0,0 +1,12 @@
# Pending attribution rows
A pull request that adds an asset adds **one new file here** instead of a row in `ATTRIBUTION.md`,
so two pull requests never edit the same table.
- Name it after the change, lowercase with dashes: `render-studio-example.md`.
- Contents: the rows only, in the format of the files table in `ATTRIBUTION.md`
(`| File | Author | Source | Licence |`); a header line is allowed and ignored.
- A file that already has a row in `ATTRIBUTION.md` or another pending file fails
`cargo xtask lists`. `cargo xtask assets` counts pending rows as attributed.
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it appends
the rows to `ATTRIBUTION.md` and deletes the files. Never run it in a pull request.
+2 -12
View File
@@ -1,5 +1,5 @@
//! Presentation/projection state for an exact selected planar source placement.
use crate::{DocError, Document, FaceAnchor, ModelBody, ModelState, Result};
use crate::{DocError, Document, FaceAnchor, ModelState, Result};
use std::borrow::Cow;
pub fn state_in_face_anchor_frame<'a>(doc: &Document, state: &'a ModelState, anchor: &FaceAnchor, author: u64) -> Result<Cow<'a, ModelState>> {
@@ -9,17 +9,7 @@ pub fn state_in_face_anchor_frame<'a>(doc: &Document, state: &'a ModelState, anc
let mut output = crate::state_in_frame_at(doc, state, author, frame).into_owned();
// In place, the body is the model's own, display mesh included: a sketch projecting the
// face's edges meshes it once for the features after it too, rather than a copy.
let mut body = if crate::is_identity(&relative) {
raw.clone()
} else {
ModelBody::new(raw.name.clone(), solidcraft_kernel::transform_matrix(&raw.body, relative)?, raw.feature)
};
body.component = raw.component;
body.transferred = raw.transferred;
body.source_name = raw.source_name.clone();
body.source_transform = crate::mat_mul(&relative, &raw.source_transform);
body.imported = raw.imported.clone();
body.names = crate::naming::NameCell::same(raw);
let body = state.body_in_frame(raw, &relative)?;
let slot = output.bodies.iter_mut().find(|body| body.name == raw.name).ok_or_else(|| DocError::Unknown("anchored body frame".into()))?;
*slot = body;
Ok(Cow::Owned(output))
+1 -1
View File
@@ -220,7 +220,7 @@ impl Document {
let (p, look) = (crate::apply_point(&b.source_transform, f.point), f.look.clone());
if f.strict_name {
let Some(name) = f.name.as_ref() else { continue };
let Some(origin_feature) = name.strip_prefix('F').and_then(|name| name.split_once(':')).and_then(|(id, _)| id.parse().ok()) else {
let Some(origin_feature) = crate::persistent_name::made_by(name) else {
continue;
};
let target = crate::config::ConfigFaceTarget {
+23 -12
View File
@@ -146,6 +146,10 @@ fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
*at = apply_point(m, *at);
}
PlaneRef::Offset { base, .. } | PlaneRef::Flipped { base, .. } => map_plane_ref(m, base),
PlaneRef::ThroughPoint { base, point } => {
map_plane_ref(m, base);
map_geo_ref(m, point);
}
PlaneRef::AtAngle { base, axis_origin, axis_dir, .. } => {
map_plane_ref(m, base);
*axis_origin = apply_point(m, *axis_origin);
@@ -223,6 +227,22 @@ impl FeatureKind {
}
}
/// [`Document::component_transform`] with each component's first occurrence found by `first`
/// (an index, where many transforms are wanted at once).
pub(crate) fn transform_through<'a>(component: u64, first: impl Fn(u64) -> Option<&'a Occurrence>) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = first(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
}
impl Document {
/// The first occurrence of a component (its placement for authoring).
pub fn occurrence_of(&self, component: u64) -> Option<&crate::Occurrence> {
@@ -231,17 +251,7 @@ impl Document {
/// World transform of a component's frame (through its first occurrence and its parents').
pub fn component_transform(&self, component: u64) -> Mat {
let mut m = IDENTITY;
let mut c = component;
for _ in 0..1000 {
if c == 0 {
break;
}
let Some(o) = self.occurrence_of(c) else { break };
m = mat_mul(&o.transform, &m);
c = o.parent;
}
m
transform_through(component, |c| self.occurrence_of(c))
}
/// One placement: the component the occurrence `path` from the root leads to (the root for
@@ -293,8 +303,9 @@ impl Document {
/// Body frames with explicit definition names; callers must not infer them from labels.
pub fn body_frames_with_source(&self, st: &crate::ModelState) -> Vec<(String, String, u64, Vec<u64>, Mat)> {
let mut out = Vec::new();
let owners = self.model_body_components(st);
for (comp, path, m) in self.placements() {
for b in st.bodies.iter().filter(|b| self.model_body_component(b) == comp) {
for (b, _) in st.bodies.iter().zip(&owners).filter(|(_, owner)| **owner == comp) {
let name = self.world_body_name(st, &b.name, comp, &path);
let w = match self.model_body_offset(b) {
Some(o) => mat_mul(&m, o),
+17
View File
@@ -3,6 +3,7 @@
use std::collections::BTreeMap;
use crate::persistent_name::{Base, FaceName, FeatureId};
use crate::{FeatureKind, PlaneRef};
/// Remap persistent topology tokens without rewriting arbitrary user expressions or labels.
@@ -22,6 +23,14 @@ pub fn remap_topology(name: &str, ids: &BTreeMap<u64, u64>, bodies: &BTreeMap<St
{
return format!("{new}:face:{suffix}");
}
// A name that reads is remapped by its structure: every feature it names, and a body
// face's body. Text of no known form is remapped token by token below.
if let Ok(face) = FaceName::parse(name)
&& !matches!(face.base, Base::Other(_))
&& let Some(mapped) = face.map(&mut |id, _| Some(ids.get(&id.0).map_or(id, |n| FeatureId(*n))), &mut |b| bodies.get(b).cloned())
{
return mapped.to_string();
}
let mut out = String::new();
let mut at = 0;
while at < name.len() {
@@ -106,6 +115,10 @@ fn remap_plane(p: &mut PlaneRef, ids: &BTreeMap<u64, u64>, bodies: &BTreeMap<Str
}
match p {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { 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::Face { name, anchor, .. } => {
if let Some(name) = name {
*name = remap_topology(name, ids, bodies);
@@ -285,6 +298,10 @@ fn checked_plane(p: &PlaneRef, depth: usize) -> crate::Result<()> {
checked_geo(plane, depth + 1)?;
checked_geo(line, depth + 1)?;
}
PlaneRef::ThroughPoint { base, point } => {
checked_plane(base, depth + 1)?;
checked_geo(point, depth + 1)?;
}
PlaneRef::ThreePoints { a, b, c } => {
checked_geo(a, depth + 1)?;
checked_geo(b, depth + 1)?;
+56
View File
@@ -0,0 +1,56 @@
use serde_json::{Value, json};
use crate::FeatureKind;
fn kind(v: Value) -> FeatureKind {
serde_json::from_value(v).unwrap()
}
#[test]
fn an_old_coil_saves_unchanged() {
let old = json!({
"type": "coil",
"base": [0.0, 0.0, 0.0],
"axis": [0.0, 0.0, 1.0],
"diameter": "40",
"pitch": "8",
"turns": "4",
"section_size": "3",
"section": "circular",
"operation": "new_body"
});
assert_eq!(serde_json::to_value(kind(old.clone())).unwrap(), old);
}
#[test]
fn a_coil_from_a_sketch_saves_its_sketch_and_profiles() {
let v = json!({
"type": "coil",
"base": [0.0, 0.0, 0.0],
"axis": [0.0, 0.0, 1.0],
"sketch": 7,
"profiles": {"type": "indices", "indices": [0]},
"pitch": "8",
"turns": "4",
"section": "circular",
"operation": "new_body"
});
assert_eq!(serde_json::to_value(kind(v.clone())).unwrap(), v);
}
#[test]
fn all_profiles_are_omitted() {
let v = json!({
"type": "coil",
"base": [0.0, 0.0, 0.0],
"axis": [0.0, 0.0, 1.0],
"sketch": 7,
"pitch": "8",
"turns": "4",
"section": "circular",
"operation": "new_body"
});
let saved = serde_json::to_value(kind(v.clone())).unwrap();
assert_eq!(saved, v);
assert!(saved.get("profiles").is_none());
}
+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
+93 -11
View File
@@ -526,6 +526,89 @@ impl ExactSketchCurves<'_> {
}
}
impl ExactSketchCurves<'_> {
/// The roles of a thin wall's side that runs parallel to some of the wall's `curves`, off
/// them by one of the wall's `distances`: `wall:<curve>:left` (or `:right`) for the side of
/// that curve, left or right of the direction it is drawn in. A thin wall's sides lie off the
/// sketch (by half its thickness, say), so [`find_all_along`](Self::find_all_along) finds no
/// curve under them. Every curve the side runs along and overlaps names it (the first its
/// name, the others aliases), in the sketch's order; empty when it is parallel to none, or
/// its distance is not one of the wall's.
pub(crate) fn find_offset(&self, generated: &Seg2, curves: &[String], distances: [f64; 2]) -> Vec<String> {
let wanted = |offset: f64, scale: f64| {
let tolerance = 1e-7 * scale.max(1.);
offset.abs() > tolerance && distances.iter().any(|d| (d - offset.abs()).abs() <= tolerance)
};
let side = |offset: f64| if offset > 0. { "left" } else { "right" };
let tau = std::f64::consts::TAU;
let mut roles = Vec::new();
for (id, _, originals) in &self.segments {
if !curves.iter().any(|c| c == id) {
continue;
}
let found = originals.iter().find_map(|original| match (*original, *generated) {
(Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => {
let direction = b - a;
let length = direction.len();
let scale = length.max((d - c).len());
if length <= 1e-12 {
return None;
}
let (left_c, left_d) = (direction.cross(c - a) / length, direction.cross(d - a) / length);
if (left_c - left_d).abs() > 1e-9 * scale.max(1.) || !wanted(left_c, scale) {
return None;
}
let (tc, td) = ((c - a).dot(direction) / direction.len2(), (d - a).dot(direction) / direction.len2());
(tc.min(td).max(0.) < tc.max(td).min(1.) - 1e-9).then(|| side(left_c))
}
(Seg2::Arc { center: oc, radius: or, start: os, sweep: ow }, Seg2::Arc { center: gc, radius: gr, start: gs, sweep: gw }) => {
let scale = or.max(gr);
if oc.dist(gc) > 1e-9 * scale.max(1.) || !wanted(gr - or, scale) || ow == 0. || gw == 0. {
return None;
}
// Drawn counter-clockwise, the curve's left is towards its centre.
let left = if ow > 0. { gr < or } else { gr > or };
let inside = |start: f64, sweep: f64, angle: f64| {
let low = if sweep >= 0. { start } else { start + sweep };
(angle - low).rem_euclid(tau) <= sweep.abs() + 1e-9
};
let overlaps = inside(os, ow, gs)
|| inside(os, ow, gs + gw)
|| inside(os, ow, gs + gw / 2.)
|| inside(gs, gw, os)
|| inside(gs, gw, os + ow / 2.)
|| ow.abs() >= tau - 1e-9
|| gw.abs() >= tau - 1e-9;
overlaps.then_some(if left { "left" } else { "right" })
}
_ => None,
});
// The end of an open chain: a straight cut across the curve, through its end point.
let cap = || {
let Seg2::Line { a: c, b: d } = *generated else { return None };
let width = (d - c).len();
if width <= 1e-12 || (width - (distances[0] + distances[1])).abs() > 1e-7 * width.max(1.) {
return None;
}
originals.iter().find_map(|original| {
[("start", original.start(), original.start_tangent()), ("end", original.end(), original.end_tangent())].into_iter().find_map(
|(end, point, tangent)| {
let across = (d - c) * (1. / width);
let on_line = across.cross(point - c).abs() <= 1e-7 * width.max(1.);
let along = (point - c).dot(across);
(tangent.dot(across).abs() <= 1e-7 && on_line && (-1e-7..=width + 1e-7).contains(&along)).then_some(end)
},
)
})
};
if let Some(side) = found.or_else(cap) {
roles.push(format!("wall:{id}:{side}"));
}
}
roles
}
}
fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
match (original, generated) {
(Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => {
@@ -580,18 +663,14 @@ impl Document {
| Some(solidcraft_kernel::FaceSource::ShellPlanar { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), .. })
| Some(solidcraft_kernel::FaceSource::CylinderCap { producer, .. }) => *producer,
_ => name
.strip_prefix('F')
.and_then(|name| name.split_once(':'))
.and_then(|(id, _)| id.parse().ok())
.ok_or_else(|| DocError::Invalid("face source producer missing".into()))?,
_ => crate::persistent_name::made_by(&name).ok_or_else(|| DocError::Invalid("face source producer missing".into()))?,
};
let certificate = self.configuration_face_certificate(body, index)?;
if let Some(FaceCertificate::ExtrudeSide { curve, .. }) = &certificate {
name = format!("F{origin}:side:{curve}");
} else if let Some(FaceCertificate::ShellPlanar { source, inner, .. }) = &certificate {
name = format!("F{origin}:{}", shell_certificate_role(source, *inner)?);
} else if !name.starts_with(&format!("F{origin}:")) {
} else if crate::persistent_name::made_by(&name) != Some(origin) {
name = format!("F{origin}:face:{index}");
}
let inverse = crate::mat_inverse(&body.source_transform).ok_or_else(|| DocError::Invalid("invalid definition placement".into()))?;
@@ -725,7 +804,7 @@ impl Document {
}
let full = names.faces.get(index)?;
let stem = crate::naming::strip_piece(full);
let origin = stem.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
let origin = crate::persistent_name::made_by(stem)?;
let inverse = crate::mat_inverse(&body.source_transform)?;
let mut candidates = vec![stem];
if full != stem {
@@ -819,10 +898,13 @@ impl Document {
return Ok(indices);
}
let names = crate::naming::names(body);
let own = format!("F{origin}:");
let stem = |index: usize| names.faces.get(index).map(|name| crate::naming::strip_piece(name));
let seed: Vec<usize> =
indices.into_iter().filter(|index| stem(*index).is_some_and(|name| name.starts_with(&own) && !name.contains('@'))).collect();
let seed: Vec<usize> = indices
.into_iter()
.filter(|index| {
stem(*index).is_some_and(|name| crate::persistent_name::made_by(name) == Some(origin) && !crate::persistent_name::is_copy(name))
})
.collect();
let feature = self.feature(origin).map_or_else(|| format!("F{origin}"), |feature| feature.name.clone());
let ambiguous = || DocError::Invalid(format!("the attached flat face of {feature} on `{}` is missing or ambiguous", body.name));
let first = seed.first().and_then(|index| stem(*index)).ok_or_else(ambiguous)?;
@@ -841,7 +923,7 @@ impl Document {
return indices;
}
let names = crate::naming::names(body);
let copy = |index: &usize| names.faces.get(*index).is_some_and(|name| crate::naming::strip_piece(name).contains('@'));
let copy = |index: &usize| names.faces.get(*index).is_some_and(|name| crate::persistent_name::is_copy(name));
if indices.iter().all(copy) {
return indices;
}
+7 -7
View File
@@ -1,7 +1,7 @@
//! Configuration graph remapping for independent component copies.
use super::{Column, ConfigTable};
use crate::{DocError, Result};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, Default)]
pub struct ConfigCloneMaps {
@@ -69,11 +69,7 @@ impl ConfigTable {
maps.bodies.get(&target.body).zip(maps.features.get(&target.origin_feature)).map(|(body, origin)| {
let mut target = target.clone();
target.body = body.clone();
target.name = target
.name
.strip_prefix(&format!("F{}:", target.origin_feature))
.map(|role| format!("F{origin}:{role}"))
.unwrap_or_else(|| target.name.clone());
target.name = crate::persistent_name::map_features(&target.name, &maps.features).unwrap_or_else(|| target.name.clone());
target.origin_feature = *origin;
if let Some(certificate) = &mut target.certificate {
certificate.remap_features(&maps.features);
@@ -106,6 +102,9 @@ impl ConfigTable {
}
}
}
// The ids taken so far, looked up rather than scanned: copies of rules whose ids share
// their first 40 characters try one id after another.
let mut taken: BTreeSet<String> = next.rules.iter().map(|rule| rule.id.clone()).collect();
for rule in &self.rules {
if let Some(mut copied) = rule.clone_for_targets(maps) {
if next.rules.len() >= super::MAX_RULES {
@@ -114,8 +113,9 @@ impl ConfigTable {
let stem: String = rule.id.chars().take(40).collect();
copied.id = (1..=super::MAX_RULES + 1)
.map(|i| format!("{stem}_copy{i}"))
.find(|id| !next.rules.iter().any(|rule| rule.id == *id))
.find(|id| !taken.contains(id))
.ok_or_else(|| DocError::Invalid("no configuration rule ID available".into()))?;
taken.insert(copied.id.clone());
copied.name = format!("{} (copy)", rule.name.chars().take(120).collect::<String>());
next.rules.push(copied);
}
+68 -3
View File
@@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
pub const MAX_RULES: usize = 128;
/// Most configuration rules a design keeps. A rule drives one feature's suppression, so an
/// assembly that switches a few hundred parts on and off, by two rules each, needs hundreds.
pub const MAX_RULES: usize = 2048;
const MAX_NODES: usize = 64;
const MAX_DEPTH: usize = 8;
@@ -235,7 +237,61 @@ fn condition(doc: &Document, values: &BTreeMap<String, expr::Value>, value: &Rul
}
}
/// The features whose suppression a condition reads.
fn suppression_reads(c: &RuleCondition) -> Vec<u64> {
match c {
RuleCondition::Always | RuleCondition::Parameter { .. } => Vec::new(),
RuleCondition::All { conditions } | RuleCondition::Any { conditions } => conditions.iter().flat_map(suppression_reads).collect(),
RuleCondition::Not { condition } => suppression_reads(condition),
RuleCondition::Suppression { feature, .. } => vec![*feature],
}
}
impl Document {
/// After a plain parameter edit (`before`: the parameter values before it): the enabled rules
/// that switch a feature's suppression and whose condition reads a parameter whose value
/// changed, or the suppression of a feature switched earlier in this pass, applied in their
/// order as activation applies them. A switch parameter alone so switches its features.
/// Driven parameters and restrictions still wait for a configuration edit or activation, and
/// a rule whose condition reads nothing that changed leaves its feature as it is. A rule that
/// cannot be evaluated (a missing parameter) is skipped: it never blocks the edit.
/// Returns the features switched, with their new suppression.
pub fn apply_suppression_rules(&mut self, before: &BTreeMap<String, expr::Value>) -> Vec<(u64, bool)> {
if !self.configs.rules.iter().any(|r| r.enabled && matches!(r.effect, RuleEffect::DriveSuppression { .. })) {
return Vec::new();
}
let (values, _) = self.param_values();
let changed: BTreeSet<&String> = values
.iter()
.filter(|(name, v)| before.get(*name).is_none_or(|b| !close(b.v, v.v) || b.len != v.len || b.ang != v.ang))
.map(|(name, _)| name)
.chain(before.keys().filter(|name| !values.contains_key(*name)))
.collect();
if changed.is_empty() {
return Vec::new();
}
let mut switched: Vec<(u64, bool)> = Vec::new();
for rule in self.configs.rules.clone() {
let RuleEffect::DriveSuppression { feature, value } = rule.effect else { continue };
if !rule.enabled {
continue;
}
let reads = rule.parameter_references().iter().any(|n| changed.contains(n))
|| suppression_reads(&rule.condition).iter().any(|f| switched.iter().any(|(s, _)| s == f));
if !reads || !condition(self, &values, &rule.condition, rule.length_unit.as_deref()).unwrap_or(false) {
continue;
}
if let Some(f) = self.feature_mut(feature)
&& f.suppressed != value
{
f.suppressed = value;
switched.retain(|(s, _)| *s != feature);
switched.push((feature, value));
}
}
switched
}
/// Apply ordered drives, then enforce all matched restrictions on the final state.
/// Running on a scratch document makes a failing rule fully atomic for callers.
pub(super) fn evaluate_configuration_rules(&mut self) -> Result<Vec<RuleTrace>> {
@@ -572,7 +628,16 @@ mod tests {
.unwrap();
assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":[valid.clone()]}})).is_ok());
assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":null}})).is_err());
assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":vec![valid.clone();129]}})).is_err());
// An assembly that switches a few hundred parts on and off, by two rules each, keeps them.
let many: Vec<Value> = (0..600)
.map(|i| {
let mut r = valid.clone();
r["id"] = serde_json::json!(format!("r{i}"));
r
})
.collect();
assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":many}})).is_ok());
assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":vec![valid.clone();MAX_RULES + 1]}})).is_err());
let mut condition = serde_json::json!({"type":"always"});
for _ in 0..8 {
condition = serde_json::json!({"type":"not","condition":condition});
@@ -636,7 +701,7 @@ mod tests {
Value::Null,
serde_json::json!([deep]),
serde_json::json!([wide]),
serde_json::json!(vec![valid; 129]),
serde_json::json!(vec![valid; MAX_RULES + 1]),
serde_json::json!([invalid_context]),
] {
let mut saved = serde_json::to_value(design()).unwrap();
+6 -5
View File
@@ -122,12 +122,13 @@ impl Document {
/// Certify named generator lineage; generic index-derived naming is unsupported.
pub fn certify_config_face(&self, target: &ConfigFaceTarget) -> Result<()> {
target.validate()?;
let prefix = format!("F{}:", target.origin_feature);
let role = target.name.strip_prefix(&prefix).ok_or_else(|| DocError::Invalid("face lineage does not match its feature".into()))?;
let role = crate::persistent_name::split_feature(&target.name)
.filter(|(id, _)| *id == target.origin_feature)
.map(|(_, role)| role)
.ok_or_else(|| DocError::Invalid("face lineage does not match its feature".into()))?;
// An exact history name may name a piece by the faces around it (`F2:top#[F2:side:l4,…]`),
// never by a number (an older design's piece, or a tie the faces around it left).
let numbered =
target.name.match_indices('#').any(|(i, _)| target.name.get(i + 1..).is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())));
let numbered = crate::persistent_name::is_numbered_piece(&target.name);
let lineage_piece = matches!(target.certificate, Some(super::FaceCertificate::Lineage)) && !numbered;
if crate::naming::strip_piece(&target.name) != target.name && !lineage_piece {
return Err(DocError::Invalid("configure a face lineage, not a numbered split piece".into()));
@@ -316,7 +317,7 @@ impl Document {
}
return Ok(indices);
}
if target.name.ends_with(":side") {
if crate::persistent_name::is_wall(&target.name) {
if indices.len() != 1 {
return Err(DocError::Invalid("split curved face lineage is not yet certified".into()));
}
+22
View File
@@ -99,6 +99,18 @@ pub enum ConstructGeom {
Point(Vec3),
}
impl ConstructGeom {
/// The geometry moved by `m` (where its component is placed).
pub fn placed(&self, m: &crate::Mat) -> ConstructGeom {
let (point, vector) = (|p: Vec3| crate::apply_point(m, p), |v: Vec3| crate::apply_vector(m, v));
match self {
ConstructGeom::Plane(p) => Plane::new(point(p.origin), vector(p.x), vector(p.y)).map_or_else(|| self.clone(), ConstructGeom::Plane),
ConstructGeom::Axis { origin, dir } => ConstructGeom::Axis { origin: point(*origin), dir: vector(*dir) },
ConstructGeom::Point(p) => ConstructGeom::Point(point(*p)),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Construct {
pub feature: u64,
@@ -580,6 +592,11 @@ pub(crate) fn resolve_geo_plane(ctx: &Ctx, p: &PlaneRef) -> Result<Plane> {
let (p, tan) = along(&path_points(ctx, path)?, t).ok_or_else(|| bad("the path has no length"))?;
Plane::from_normal(p, tan).ok_or_else(degenerate)
}
PlaneRef::ThroughPoint { base, point: at } => {
let b = ctx.doc.resolve_plane_in(ctx.vals, base, ctx.depth + 1, Some(ctx.st))?;
let n = b.normal().normalized().ok_or_else(degenerate)?;
Ok(Plane { origin: b.origin + n * (point(ctx, at)? - b.origin).dot(n), ..b })
}
_ => ctx.doc.resolve_plane_in(ctx.vals, p, ctx.depth + 1, Some(ctx.st)),
}
}
@@ -688,6 +705,11 @@ impl PlaneRef {
PlaneRef::ThreePoints { a, b, c } => vec![a.as_mut(), b.as_mut(), c.as_mut()],
PlaneRef::Tangent { face, .. } => vec![face.as_mut()],
PlaneRef::AlongPath { path, .. } => vec![path.as_mut()],
PlaneRef::ThroughPoint { base, point } => {
let mut refs = base.refs_mut();
refs.push(point.as_mut());
refs
}
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { 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}");
}
}

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