Compare commits

...

138 Commits

Author SHA1 Message Date
selune 0cbf328bab chore: preserve reviewed X4-exact-nano source proposal and provenance 2026-10-31 04:37:43 +01:00
selune 3559964792 archive: conditional X3 SPLINE acceptance preserving rmorgans prerequisites (unrun) 2026-10-31 03:20:37 +01:00
selune dfb0b3e113 archive: preserve reviewed eleven-path X1 source reconciliation (unresolved lock, unrun) 2026-10-31 01:13:00 +01:00
selune cd39f96d33 archive: preserve reviewed X1 two-file reconciliation on frozen main (unrun source) 2026-10-31 00:59:27 +01:00
selune 67cbfe941e wip(drawings): preserve explicit PDF widths following the owner contract 2026-10-31 00:06:11 +01:00
selune 45286e0489 wip(drawings): preserve F0 checked font-plan sources and original acceptance 2026-10-30 23:33:09 +01:00
selune 5f127aceb0 wip(drawings): preserve additive B1 width and many-page reference source 2026-10-30 22:14:51 +01:00
selune e717739735 wip(drawings): preserve B1 text-plan scaffold and 13 original unrun cases 2026-10-30 20:44:35 +01:00
selune 28d90fae50 fix(drawings): preserve reviewed caller-local circle compatibility proposal 2026-10-30 15:06:40 +01:00
selune 7b18fd5c0f Preserve P2 B1 adaptation analysis and unit-neutral arc diagnostic 2026-10-30 10:30:14 +01:00
selune 992466e76a Preserve unintegrated W10 same-units half-turn converter and PDF reproducer proposals 2026-10-30 10:12:41 +01:00
selune c46fd3acd4 Preserve W21 NoPictures parity and repeatability source proposal 2026-10-30 08:30:35 +01:00
lillith 7d66a4d0ab Clarify Drawings preservation manifest and published index hash
Source preservation only. The manifest now records the actual published index README rather than the initial selection note, and states its pre-Git-normalization hash scope. Original product proposals are unchanged. The canonical index links all branch checkpoints; validation and owner handoffs remain pending.
2026-10-29 11:00:46 +01:00
lillith 0b3bc6e968 Preserve remaining Drawings original source proposals and canonical code index
Canonical preservation of W10, W21, X4 and supporting W38 source proposals. Sources remain deliberately unregistered and unexecuted, with real bases and alternatives documented in proposals/drawings/README.md. Existing contributor claims and PRs remain authoritative; this is not a merge-ready integration candidate.
2026-10-29 10:57:57 +01:00
lillith d8e7a8690a fix(dxf): keep counter and copy tests compatible with strict Clippy
Use fresh local mutable counter values instead of mutable references to
constants. Enumerate the ten original copied shapes after asserting their
independent fixture count. Preserve all production code, numeric expected
values, refusals and the other tests. No lint suppression or ignored tests.

The prior 6b39ee36 attempt stopped at test-target Clippy, before native
test builds/runs and wasm. Both test-only hunks were independently reviewed;
normal formatting passed without changing bytes. New results are pending.
2026-10-29 03:17:21 +01:00
lillith 6b39ee3686 fix(dxf): resolve strict Clippy findings without weakening checks
Remove an unused private Interval helper and collapse the copied-vertex
guard to an equivalent let-chain with identical evaluation order and error.
Preserve all 28 geometry and 10 bounds tests, limits and assertions.

The prior a7f3c78b development attempt stopped at Clippy; IO/engine tests
and wasm were not reached. Both corrective hunks were source-reviewed,
and normal formatting passed. Tests for this new source remain unrun.
2026-10-29 02:59:05 +01:00
lillith a7f3c78b07 feat(dxf): add bounded bulged paths and layered text import (WIP #5319 #5320)
Add the reviewed minimal X1 bulged-path producer, conservative bounds,
ordered layered bulge reader, ELLIPSE/TEXT imports and two original
cross-feature cases. Preserve legacy entry points; retain the two explicit
source-supported fixture migrations, not a blanket assertion relaxation.

Include the three original owner corrections from #8534 to unblock wasm,
without adopting later main or transferring upstream validation.

Normal cargo fmt --all passed. 223 declarations in the nine IO files
(333 including 110 unchanged UI/web declarations) are not test results.
This WIP has not been compiled or run; full CI and caller migration remain.
Earlier-source native passes do not qualify this candidate.

Co-authored-by: Ray <raykholo@users.noreply.github.com>
2026-10-29 02:39:19 +01:00
lillith 471e17f50e feat(dxf): preserve layer-zero imports and nine TEXT alignments (WIP #5319 #5320)
Add explicit alignment production and bounds callbacks, immutable layer-zero
initialization/import, and a reviewed fix retaining its metadata through copies.
Preserve the existing POINT and real writer-reader roundtrip foundation.

156 test declarations across four modules, including 26 additions against the
published parent; this source has NOT been formatted, compiled or run. Separate
source reviews do not qualify runtime. The older-source X2 gate remains separate.
2026-10-28 23:29:45 +01:00
lillith 1891344e07 feat(dxf): preserve POINT and original round-trip acceptance (WIP #5320)
Source-only reviewed four-file increment. 18 added test declarations, not executed. Published at the user's request; current X2 validation continues on unchanged older source.
2026-10-28 22:54:24 +01:00
lillith 1e176d8503 feat(dxf): preserve partial layered reader WIP (#5320)
Stacked on the owned X2 model. Includes 52 declared tests, not executed. Legacy projection and the remaining importer contract are pending; not ready for merge.
2026-10-28 21:57:39 +01:00
lillith fac0043543 feat(dxf): preserve owned file model and shared writer WIP (#5319)
Publish the reviewed partial implementation at the user's request while the corrected normal full gate is running. Not ready or fully validated.
2026-10-28 21:57:39 +01:00
lillith 119c40c814 claim: #5319 2026-10-28 19:37:38 +01:00
github-actions[bot] fe3fb381b1 Regenerate generated files and fold pending files [skip ci] 2026-10-28 18:21:33 +00:00
github-actions[bot] 0778d24801 Regenerate generated files and fold pending files [skip ci] 2026-10-28 18:14:57 +00:00
PaddleStroke 79f53a0999 Sketch dimensions in their own colour in every theme (#7861) (#8124)
* claim: #7861

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

* Sketch dimensions in a colour of their own in every theme (#7861)

Dimension lines, arrows and values were drawn in the same near-black or
near-white as fully constrained sketch curves, so they were hard to tell
apart in a busy sketch. Every theme now sets its own dimension colours:
plum on Light, orchid on Dark, and a hue from each built-in theme's own
palette. Colour vision tests measure them against the curves, the
construction lines and the conflict red, and a new test holds every
shipped theme to readable, distinct dimension colours.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:23:47 +00:00
froglemonade2 dba3a226f6 Closing from the background brings the window forward to ask about unsaved changes (#7783) (#7784)
* Closing from the background: the window comes forward to ask about unsaved changes

Right-click › Close on the taskbar while another app had the focus showed the
"save changes?" prompt in a window that stayed behind, so the close seemed to do
nothing. When the close stops at the prompt, the window now asks for the focus
(unminimizing it first) and for the user's attention, which is what native
Wayland offers (xdg-activation) and marks the taskbar entry where the system
refuses the focus. A window already in focus is left alone.

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

* X11: ask the window manager to raise the window as a user action

winit's Focus asks as an application, which KWin's focus-stealing prevention
refuses for a window last used before the switch away: it raised nothing and,
since the attention request was then skipped, did not even mark the taskbar.
On X11 send _NET_ACTIVE_WINDOW with source 2 (a direct user action) through
x11rb, which restores a minimized window too; native Wayland keeps the
attention request. Moves the logic into come_forward.rs.

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

* Come forward once the taskbar menu is gone, without flicker

KWin raised the window, then the taskbar's closing context menu handed the
focus back to the previous app 2 to 11 ms later, so the window only came
forward some of the time; asking again made it flicker. Ask 120 ms after the
close instead, and again only when the focus is taken back within 50 ms of a
request (a slow menu) or never came. A later switch away is the user's own and
the window stays behind.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:09:25 +00:00
cronos b72165fd30 Sketch: show temporary centre and midpoint alignment guides (#7903)
* Sketch: show center and midpoint alignment guides

Add transient per-viewport horizontal and vertical guides with a sketch preference. Related tests, native UI, WASM and supplementary Linux checks pass. Full CI remains blocked by unchanged Windows engine dead-code Clippy errors.

* Sketch: complete alignment-guide translations

* Tests: compile chosen-program mutex only on Unix
2026-10-28 16:07:05 +00:00
Frank 8e26fe9388 Measure: number the two picks in the view (#6020) (#6021)
* claim: #6020

* Measure: number the two picks in the view (#6020)

"1" and "2" beside the measured line's ends, as the panel lists the
picks, each just past its end so it stays off the line and its length.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 16:05:22 +00:00
semmelrinde c8c9534022 Create Sketch: the grid previews the origin plane under the pointer (#6975) (#7404)
* claim: #6975

* Create Sketch: the grid previews the plane under the pointer (#6975)

While Create Sketch waits for its plane, the grid now lies on the origin or
construction plane under the pointer, so the sketch's plane shows before
the click. A plane's browser row under the pointer lights that plane in the
view, and previews it the same way.

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

* Create Sketch: the planes at the top of the History preview too (#6975)

main now draws the Origin rows at the top of the vertical History as well
(history_rows). Hovering a plane there lights it like its row in the browser:
the plane is handed over the same way the hovered bodies are.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:58:33 +00:00
Christian Hammacher aae2c5e417 Assemblies: edit a linked component in place, with the assembly around it (#7672) (#7819)
* Links: update linked designs that changed while the assembly's tab was away

A design coming back to its tab compares the modification time and size of its linked
design files with what they were when it went to the background, and runs Update Links
on the worker thread for those that changed: saved in another tab, another window or by
a checkout. Preference update_changed_links (Design, Linked designs) turns it off.

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

* Docs: the assembly guide says linked designs update when their tab returns

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

* Links: Vietnamese strings, now that vi is a complete catalog

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

* Links: remember what each design last read, so an update that fails is tried again

A design's linked files are now recorded as the design last read them: when Update Links reads a
link without an error, or when the link is first seen. Coming back to the tab reads again what
differs, one link per run, so a file that cannot be read holds up no other and is tried again
the next time, and a part changed while the assembly was shown is taken up on its next return.
Closing several tabs together reads nothing on the way, and the web build, which keeps no file
times, looks at nothing.

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

* Links: no read during a save-changes prompt, records for adopted designs and missing files

A tab shown to answer "save changes?" reads no link before its save, and Cancel there or on a
batch lets the tab left in front read what it skipped. A design opened in place of the blank one
records its links at once, a link whose file is missing is recorded as missing so it is read when
the file comes back, and a read queued for a design that is no longer shown by the time it starts
is dropped. More changed links than can wait at once are read on the next return.

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

* Assemblies: edit a linked component in place, with the assembly around it

Activating a linked component opens the design it comes from in a tab of its own, with the
rest of the assembly drawn faded around it where the component sits, so the part's own history,
browser and commands are at hand. Finish Editing in the browser saves the part, goes back to the
assembly and closes the part's tab when the edit opened it; coming back to the assembly updates
the changed link (or Finish Editing runs Update Links when that is turned off). Update Links in
the part also reads the assembly's links again, off the window thread, and redraws the assembly
around the part; a result for an assembly changed meanwhile is dropped with a message.

The part is an ordinary tab, so saving, autosave, recovery, closing and quitting treat it as any
other design. The model scene's ghost triangles are now drawn (only the preview's were).
Preference edit_linked_in_place turns it off. Builds on the update of changed links when an
assembly's tab returns.

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

* Edit in place: a double-click opens it, Activate only activates

A double-click on a linked component edits it in place, in the browser as on a face in the
view (around the placement clicked), and the component's menu gets Edit in Place. Activate and
the browser's radio button make it the active component as they always did, so a linked
component can still be activated without opening its design.

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

* Double-click on a component's face: edit a linked one in place, activate any other

A double-click on a face in the view activates the component the body belongs to, at that
placement, as a double-click on its row does; a linked one is edited in place. Bodies of the
design itself or of the active component are left alone, and so is a double-click with a
modifier. One preference, double_click_components, covers both gestures.

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

* Edit in place: fixes from review

An edit whose part never opens is forgotten instead of starting on a later open of the same
file, and the web build starts the edit after its synchronous open. Finish Editing goes on only
for a save into the linked file and in the part's own tab, waits for a "changed on disk"
answer, and says so when the part was saved elsewhere. Showing the assembly no longer drops a
refresh, which now records the links it read so coming back reads nothing twice. The faded
assembly is worked out once per picture, a bad mesh index skips its whole triangle, and the
last two status texts come from the catalogs.

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

* Edit in place: Finish Editing on the MODEL tab and the mouse's back button

Finish Editing is a command now, component.finish_edit, with its schemas: the app window
answers it, anywhere else nothing changes, as with view.fit. While a linked design is edited in
place the MODEL tab ends with a FINISH EDITING panel, pinned at the right as FINISH SKETCH is,
with a new left-arrow icon; the browser keeps only the line naming the assembly. The mouse's
back button, or a keyboard's Back key, finishes too unless a dialog, tool or sketch is open.
The assembly guide describes editing in place.

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

* Edit in place: fixes from the second review

Saving the assembly waits while it reads its links for the part
Finish Editing waits for dialogs, tools, sketches and prompts open in the part, also when run as a command
The assembly reads the edited part explicitly when it does not check its links on its own
The FINISH EDITING panel cannot be customized
A tab opened by an edit stays that edit's to close when the part is entered again
The mouse's back button finishes editing; the keyboard's only while no text field has focus
Refreshing the assembly keeps its selection and active component

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

* Edit in place: fixes from the third review

component.finish_edit run as a command opens nothing and answers with an error when it cannot finish; engine.batch and engine.script refuse it
A double-click or Edit in Place while a dialog, tool or sketch is open in the assembly keeps it open
What changes in the assembly while it reads its links without changing its document stays: picks, activation, components dragged but not captured
Showing the assembly while it reads its links starts no second read
A new or opened design in the part's tab ends the edit
Finish Editing keeps the part's tab open while its agent chat runs

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

* Edit in place: Update Links run as a command in the part refreshes the assembly too

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

* Edit in place: the ribbon button reads Finish Edit, as Finish Sketch does

A bare "Finish" took the catalogs' surface-finish translation (German "Oberfläche").

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

* Edit in place: fixes from the fourth review

Finish Edit waits while the part's agent is in the middle of its turn, instead of keeping the tab of any chat open
A part saved while its assembly refreshes is read again when the edit finishes: the refresh records the files as it found them
A double-click on a linked component while something is open in the assembly says so instead of activating it
Commands are recognised in any spelling the engine takes, so engine.batch and engine.script cannot slip Finish Edit through
Update Links run as a command refreshes the assembly from the control channel only, not for the app's own reads
Moves of components the refresh removed are not carried over, and a refusal is one message
The command description and the guide say that the command does not save the part

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

* Edit in place: the command is Finish Edit, as Finish Sketch is

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

* Edit in place: a command to start it, and fixes from the full review

Added component.edit_in_place {component, path?}: the double-click and Edit in Place in the menu run it, and as a command it asks nothing, so a network location not allowed yet or a design open elsewhere makes it fail instead
Finish Edit asked by the part's own agent finishes once that agent's turn ends; the button still waits for the turn
The parameters and design properties panels, with text typed and not applied yet, hold up entering and finishing an edit
A refresh is not taken up under an undo, a queued command or a feature still being computed in the assembly
Refusals say what holds them up (a refresh, a file being read or written) instead of nothing
The browser strip names the assembly that is refreshing, not any assembly
The schema description says the command does not save the part
Korean strings for every new message

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

* Edit in place: fixes from the second full review

The MCP batch tool refuses component.edit_in_place and component.finish_edit as steps instead of sending them one by one
Edit in Place asked while the assembly's agent is in its turn opens the part once the turn ends; a double-click then is refused rather than stopping the agent
Both commands answer queued: true when they wait for the agent's turn, and stopping the agent forgets what it queued
An undo, a queued command or a delete check running in the design holds up entering and finishing an edit
A refresh redraws every edit of the assembly, the parts in other tabs too
In an assembly never saved, a double-click activates the linked component as before
The control and MCP docs list the new batch and script refusals, and the commands list every way they fail

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

* Edit in place: fixes from the third full review

The MCP batch tool refuses the two commands however a step names them, an execute step too
A queued Edit in Place is checked fully when asked for, a second one is refused, and both queued requests wait for what runs in the design instead of being dropped
Disconnecting the agent forgets what it queued, as stopping it does

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

* Edit in place: help text for the Finish Edit button

main now requires help for every toolbar command (#7134), and Finish Edit
is one. Written in every language the branch's catalog carries.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:57:44 +00:00
Ajmal Musthafa 2cc6615c2d Enhance two-tangent circle tool: add default radius handling and improve interaction hints (#8116) 2026-10-28 15:57:06 +00:00
Homero Thompson 9c5b3beb73 geom: cover inside_loop and segments_meet in the fill predicates (#6162)
Add unit tests for fill.rs's two ray-casting predicates, which
fill_general.rs relies on to decide which parts of a surface a loop
covers: a point inside/outside a square and an L-shaped (non-convex)
loop, documenting how inside_loop resolves points on an edge or
vertex; and segments that cross, stay apart, touch at an endpoint,
or are collinear with and without overlap for segments_meet.

Closes #38

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-28 15:49:30 +00:00
CamWight 14acdc2a6d Render: one sRGB transfer function (#7161) (#7162)
* Render: one sRGB transfer function, geom::srgb, for the hand-written copies (#7161)

Twelve places wrote the sRGB encode or decode formula out by hand (doc's glass
recipe, engine's render library, glTF import, the PNG film, photographic and
presentation output, the ray tracer's image, the stage, the surface decoder,
texture normalisation and the theme's contrast check). They now call
solidcraft_geom::srgb: encode/decode on 0..1 values, encode8/decode8 for 8-bit
codes. render::surface::srgb8_to_linear stays as a re-export, so its callers
are unchanged.

Same results: the module's tests compare it bit for bit with the formula as the
callers wrote it, over all 256 codes and a dense sweep of linear values plus
the knees, out-of-range values, infinities and NaN, for each way the callers
clamped (after encoding, before it, or not at all), and decoded values either
side of the decode knee, where no 8-bit code falls.

One change: glTF import encoded in f32. Over every f32 in 0..1, 218 of
1,065,353,217 values sit on a rounding tie that f32 got wrong and now give the
neighbouring code (f64 gives the nearer one).

Left as they are: render/job/image.rs (replaced by exact tables in its own
change), the tests and the starter-pack validator that keep their own copy as
an independent reference, and the WGSL shaders (they can't call Rust).

Fixes #7161

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

* Tests: the sRGB codes of five callers that had no test of their output (#7161)

The glass swatch, a library surface applied as a plain appearance, glTF's
fallback for a colour channel that isn't a number, a stage panel and an empty
pixel in presentation output each get an assertion with their sRGB codes worked
out by hand. A planted wrong conversion at each is now caught by that test.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:48:56 +00:00
semmelrinde bcb67cc2ec Mirror and Pattern: a body feature reopens with Bodies and its Operation (#8207) (#8298)
Editing a body Mirror from the history showed Object Type Features (the dialog starts from
the timeline selection, the Mirror itself, which turns pattern objects into features) and
dropped its saved Join. The editor now sets the objects back to Bodies for a body Mirror or
Pattern and restores the Mirror's combine.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:38:41 +00:00
Jasper van Loenen a8e5bfca73 Fix BOM table being too narrow (#8113) (#8144)
* claim: 8113

* Bill of Materials: size columns to their contents, Description fills the rest (#8113)
2026-10-28 15:28:55 +00:00
raykholo b68d42e5ec Theme: paste a theme into the Import tile (#8226) (#8227)
* claim: #8226

* Theme: paste a theme into the Import tile (#8226)

Ctrl/Cmd+V on Preferences > Interface imports a copied theme file, the tile's
menu offers Paste theme, and a theme file dropped on the tile imports instead
of opening as a design. Pasted text that is not a theme keeps its usual
meaning. The tile reads "file or paste".

* Theme paste: the new strings in Arabic, Hebrew and Vietnamese too (#8226)
2026-10-28 15:28:48 +00:00
github-actions[bot] 1ca2c5299c Regenerate generated files and fold pending files [skip ci] 2026-10-28 17:46:27 +00:00
github-actions[bot] 7d652d0e1f Regenerate generated files and fold pending files [skip ci] 2026-10-28 17:39:36 +00:00
semmelrinde 5f98396460 Dutch: translate the Render workspace's 733 messages (batch 2) (#8283)
* Dutch: translate the Render workspace's 706 messages (batch 2)

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

* Dutch: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:28:41 +00:00
Badger B bffa846a55 Add German translations, batch 3 (#7237) (#8285)
* Add German translations, batch 3 (#7237)

* Use "Ansatz" for boss in German (#7237)

* Fix article for "Ansatz" (#7237)
2026-10-28 15:28:35 +00:00
semmelrinde 62da9deb29 Hungarian: translate the Render workspace's 733 messages (batch 2) (#8286)
* Hungarian: translate the Render workspace's 706 messages (batch 2)

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

* Hungarian: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:28:28 +00:00
semmelrinde d17bc410a9 Galician: translate the Render workspace's 733 messages (batch 2) (#8292)
* Galician: translate the Render workspace's 706 messages (batch 2)

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

* Galician: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:28:21 +00:00
semmelrinde fe210ece6d Malay: translate the Render workspace's 733 messages (batch 2) (#8299)
* Malay: translate the Render workspace's 706 messages (batch 2)

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

* Malay: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:28:06 +00:00
semmelrinde e8d214716d Norwegian Bokmål: translate the Render workspace's 733 messages (batch 2) (#8300)
* Norwegian Bokmål: translate the Render workspace's 706 messages (batch 2)

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

* Norwegian Bokmål: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:27:59 +00:00
P'tite Tête a4d1b5b6a9 Viewport: fewer ambient occlusion samples while the view moves (#7165) (#7214)
* feat: Fewer ambient occlusion samples while the view moves (#7165)

- Draw AO with 8 samples while the view moves, 32 once it has held still for 0.25 s
- Share the settle tracking with adaptive supersampling and repaint when it settles
- ui.screenshot waits for the full-quality frame (at most half a second)
- Test the sample choice, the uniform slot, and ghosting at both counts

* fix: Set the AO sample count in the scale test frame

- scale_tests.rs, new on main, builds a gpu::Frame without ao_samples
- Use AO_SAMPLES, as the other still-frame builders do

---------

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-28 15:05:34 +00:00
semmelrinde 8589cdfb84 Ukrainian: translate the catalog entries that held English, and the missing ones (#8073)
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:05:02 +00:00
dudedagger42 4a34a258e1 Logo: an option to draw it in the theme's accent (#8133)
* Logo: an option to draw it in the theme's accent

Preferences > Interface > Logo: Theme's colours (the default, as before) or
Accent, three shades of the theme's accent. ui.prefs logo_accent.

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

* Logo: US spelling for the option's label, as upstream's English catalog uses

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

* Translations for the new options in the five complete catalogs (ar, he, ja, vi, zh)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:04:53 +00:00
Herperlerpicus 3a951bc8c3 Sketch: dimension new shapes as drawn (squashed onto current main) (#5987)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:57:54 +00:00
HiroKenEz 4fa051c9fc View angles: an optional readout under the orientation gizmo and a command to set them (#6032) (#6249)
* claim: #6032

* View angles: an optional readout under the orientation gizmo and a command to set them (#6032)

View > View angles (also Preferences > Graphics, or view.angle_readout) shows the
view's pitch, yaw and roll in degrees and Ortho/Persp under the axis view or cube,
updated live; off by default. Clicking the readout opens a small card to type
exact angles, which runs view.apply.

The angles are the camera's own (Camera::view_angles / with_view_angles), about
the up axis: yaw is the heading of the line of sight (Front 0, Left 90, Right -90,
Back 180), pitch the eye's elevation (Top 90, Bottom -90), and a positive roll
turns the model clockwise. view.apply documents this convention and now refuses a
pitch past +/-pi/2.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:49:47 +00:00
Homero Thompson 31d6873f7d OBJ export: the body colours travel with the file (#4451) (#6693)
* claim: #4451

* fix(#4451): the body colours travel with the OBJ file

An OBJ export left the colours of the design behind: the mesh file named no
material, so every body arrived in the reader's default grey. The export now
writes a material library (.mtl) next to the mesh file, with a material per
body, and the mesh file names that library with mtllib and picks the material
of each body with usemtl. Every path is checked before anything is written,
the library goes first and the mesh file last, so a mesh file never names a
library that is not there. Only the colour of the body travels: the colours of
single faces and the transparency of a body stay out, as a face colour needs
material groups inside one object. The export that writes one file stays
material-free, so it never names a library it does not write.
2026-10-28 14:44:32 +00:00
github-actions[bot] 545232be37 Regenerate generated files and fold pending files [skip ci] 2026-10-28 17:08:18 +00:00
Nathan Dane c1dee61482 Render shots: strip with thumbnails, Update and Revert for camera changes (#6430) 2026-10-28 12:59:40 -04:00
github-actions[bot] ad182057b9 Regenerate generated files and fold pending files [skip ci] 2026-10-28 16:53:13 +00:00
github-actions[bot] bfe7792592 Regenerate generated files and fold pending files [skip ci] 2026-10-28 16:44:05 +00:00
semmelrinde be2deb8c6f Create Sketch: a face of a combined Mirror's copy keeps its own attachment (#5779) (#8278)
A mirror's or pattern's copy of a face carries its seed's construction certificate, and on a
combined body that certificate finds the seed's face. Capturing an attachment never checked
that the certificate finds the picked face, so a sketch on the mirrored half sat on the
original. The capture now falls back to the face's exact history name when the certificate
names another face, as docs/face-attachments.md describes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:44:32 +00:00
PaddleStroke cecb1d8b66 Naming: a long history of one body no longer overflows the stack on reorder (#7587) (#8159)
* claim: #7587

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

* Naming: name a long history of one body from the oldest up (#7587)

A body's face names are worked out from the names of the body it was made
from, recursively. After a few hundred features in a row whose names
nothing asked for, reordering or moving an earlier feature re-evaluated
them all and the first name lookup (a fillet at the end) recursed once per
feature, overflowing the 1 MB window-thread stack on Windows. Name every
16th earlier version oldest first so no lookup recurses through more than
16 of them; the names are the same.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:43:42 +00:00
semmelrinde 427bd730f0 Title bar: a longer version moves Report a bug left instead of covering it (#5621) (#8280)
The version right of Report a bug had a fixed 60 points, enough for v0.25.1 but not for a
release candidate such as v0.21.0-rc.1, whose text was painted over the button. The bar now
measures the version and reserves its width plus a gap, never less than before. The extra
width comes only from the document tabs' spare room: where they would keep less than one tab,
the version is hidden, as on a narrow bar.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:33:25 +00:00
PaddleStroke 10b8faeba3 Windows: an export over a file another program holds briefly waits for it, up to 2 s (#8104) (#8125)
* claim: #8104

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

* Windows: an export over a file another program holds briefly waits for it, up to 2 s (#8104)

write_atomic replaces the target with one rename. Windows refuses that rename
(os error 5 or 32) while another program, such as an antivirus or indexer
scanning a file written a moment earlier, has the file open without delete
sharing. The retry gave up after 127 ms of waiting, which a scan under load
outlasts, so render.export and library.export over an existing file failed
with "Access is denied".

The retry now keeps doubling its wait up to 250 ms, about 2 s in all. If the
file is still held after that, the error says the file is in use by another
program, followed by the system's own message. The old file stays whole
throughout, as before.

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

* Saving over a read-only file fails at once, and says why Windows refused

Access denied (os error 5) is also what Windows gives for a read-only or
ACL-denied target, which waiting does not fix. A read-only target is now
refused without the 2 s wait, with "the file is read-only". Any other refusal
that outlasts the wait says another program may have the file open or it is
read-only, followed by the system's own message.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:32:20 +00:00
Dane Doubell 649ab0ef91 Sketch dimension: a slot's two caps measure the centre-to-centre distance (#7464) (#7488)
* Sketch dimension: a slot's two caps measure the overall length (#7464)

* Sketch dimension: slot caps need tangent sides, not just shared lines (#7464)

* Sketch dimension: slot caps measure the centre-to-centre distance (#7464)

---------

Co-authored-by: Dane Doubell <2853036+danedoubell@users.noreply.github.com>
2026-10-28 14:23:48 +00:00
PaddleStroke 8be7bb0201 Insert design: file names get the dialog's width (#8097) (#8122)
* claim: #8097

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

* Insert design: file names get the width the size and actions leave (#8097)

The web app's "Files in this browser" window listed stored files in an egui
Grid sized from cell content: the wide action buttons took most of the fixed
window width, the name and size columns were cut, and the rest was empty.

Add ui_kit::name_list on the kit's table grid: the name column takes the
width the other columns leave, the other columns are as wide as their widest
cell (measured as drawn, a changed measurement discards the frame), and a
name is cut with an ellipsis and its full text on hover only when it does
not fit. The window is a little wider (still capped at the browser width).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:23:31 +00:00
Lars Jansen 67d5051792 Ribbon: keep the SKETCH tab shown outside a sketch (#7904) (#7906)
* claim: #7904

* feat(ui): keep the Sketch tab shown outside a sketch; its drawing tools start one (#7904)
2026-10-28 14:14:08 +00:00
Homero Thompson 7e8f87dc57 Extrude: a face shared by two bodies is resolved in the active occurrence (#419) (#6469)
* claim: #419

* fix(#419): a face shared by two bodies is resolved in the active occurrence
2026-10-28 14:13:45 +00:00
Colin Faulkingham 84016f036e Redefine: the reply reports the features it rebuilt (#5694) (#8244)
* claim: #5694

* Redefine: the reply reports the features it rebuilt (#5694)

timeline.redefine builds the edited feature and the features after it in its trial session;
the session's own evaluation then only takes those results from what the trial kept, so the
reply reported that second evaluation and read as "nothing was rebuilt" although the geometry
changed. Report the trial's rebuild, plus anything the session still had to compute itself, as
timeline.edit reports the same change; `restored` keeps counting only genuine reuse of earlier
results.

Tests: redefining_reports_the_features_it_rebuilt fails before the change with
{"feature": 2, "recomputed": 0, "restored": 2} and passes after it;
redefining_computes_the_later_features_once still guards the single computation (the reply
doubles if the kept results are lost).
2026-10-28 14:02:44 +00:00
raykholo 5f90d06452 Preferences: reverse vertical pan direction (#5240) (#5454)
* claim: #5240

* Preferences: reverse vertical pan direction (#5240)

Add "Reverse vertical pan direction" beside the horizontal setting in
Preferences > Navigation. pan_reverse_y defaults off, so older settings
files load unchanged, and flips the vertical part of two-finger pan on
every canvas through navigation::handle, independent of the horizontal
setting. Labels in en, ja, zh, de, and ko.

* Reverse vertical pan: strings as a pending locale file, with Arabic, Hebrew and Vietnamese

* Control protocol: pan_reverse_y in the sorted ui.prefs list (#5240)
2026-10-28 13:59:38 +00:00
Colin Faulkingham 27772ddba1 Keep command dialogs above Browser and History (#8174) (#8203)
* claim: #8174

* fix(ui): keep command dialogs above workspace panels (#8174)
2026-10-28 13:59:28 +00:00
semmelrinde e1b49238ba Italian: translate the Render workspace's 733 messages (batch 2) (#8250)
* Italian: translate the Render workspace's 706 messages (batch 2)

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

* Italian: the 27 Render messages main gained since (the Appearances panel)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:58:52 +00:00
github-actions[bot] ba35509ca0 Regenerate generated files and fold pending files [skip ci] 2026-10-28 16:12:44 +00:00
github-actions[bot] 32f11961e9 Regenerate generated files and fold pending files [skip ci] 2026-10-28 16:06:14 +00:00
semmelrinde 4dda001210 Traditional Chinese: translate the Render workspace's 706 messages (batch 2) (#8272)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:58:23 +00:00
semmelrinde f49d5c2f29 Indonesian: translate the Render workspace's 706 messages (batch 2) (#8271)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:58:13 +00:00
github-actions[bot] afde0f8073 Regenerate generated files and fold pending files [skip ci] 2026-10-28 15:32:53 +00:00
Nathan Dane 2897d3b22e Render library: Appearances card with a sphere grid, scopes, favourites and recents (#6429) 2026-10-28 13:41:24 +00:00
bherbruck 9bcb2ffe68 Drawings: first landing from the drawings branch (#8249)
* Drawing sheets: sizes, the ISO 5457 border and its marks (#3875) (#5108)

* Drawing sheets: sizes and the ISO 5457 border (#3875)

A sheet is a paper size and an orientation: ISO A0 to A5, Letter, Legal,
ANSI A to F, architectural A to E1, or a custom size (finite, above 0 and
at most 5,000 mm a side), landscape by default and either way for every
size.

A border profile is the rule for its border, and layout() works it out
for a sheet: the frame inside the margins, and the reference fields with
their labels. The ISO profile has a 20 mm filing margin on the left and
10 mm elsewhere, a 0.7 mm frame, fields of 50 mm out from the centre
lines with the corner fields taking what's left, rows lettered from the
top without I and O (AA, AB and on past Z), columns numbered from the
left, and an A4 sheet labelled on its top and right only. A profile can
divide the frame equally instead, or have no grid.

The records sit in geom, next to the section records, so saving,
rendering and export can all use them. Nothing is saved or drawn yet:
there is no command and no format change. Margins that leave no drawing
space, values that aren't finite, fields outside 1 to 1000 mm, more than
200 equal fields and unknown JSON fields are refused.

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

* Drawing sheets: ISO 5457's centring and trimming marks (#3875)

A drawing border profile now also holds the grid reference border and the
centring and trimming marks, either of which can be left out. The ISO
profile follows ISO 5457:1999 (read in its identical adoption,
IS 10711:2001):
- the grid reference border 5 mm outside the drawing border on every
  side, with the field labels between the two;
- four centring marks on the sheet's centre lines, from the grid
  reference border to 10 mm inside the drawing border;
- at each corner a trimming mark of two overlapping 10 x 5 mm
  rectangles, in the margin outside the grid reference border.

The labelled sides are now a rule worked out for each sheet: ISO 5457's
(the top and right of A4, all four sides of the others) or fixed sides.
So one profile labels every size correctly. validate() checks what
doesn't depend on the sheet, for when a profile is read; layout() calls
it, then checks the rest against the sheet.

Refused:
- a grid reference border that isn't finite, is negative or is wider
  than a margin;
- a centring mark that isn't above 0, would reach past the sheet's
  centre lines, or is on a drawing border those lines miss;
- a trimming mark that isn't above 0, is wider than it is long, is
  wider than the margin left outside the grid reference border, or is
  longer than half the sheet's shorter side.

The grid rule's JSON is now externally tagged ("none",
{"from_centre": {"field_mm": 50}}), as the sheet size is, so an unknown
field is refused in every form; the internally tagged "none" ignored
one. Nothing is saved yet, so the shape change costs nothing.

The names follow the glossary, and ISO 5457's "drawing sheets": the
module is drawing_sheet (was sheet, which means sheet metal elsewhere
here), DrawingBorderProfile and SheetLayout (were BorderProfile and
BorderLayout), sheet_border and drawing_border (were paper and frame),
and Sheet::size_mm (was paper_mm). The first part of S1 is still a draft,
so the renames are made here rather than in a commit of their own.

The extra centring marks ISO 5457 asks for on sheets larger than A0
aren't here yet. Still nothing is saved or drawn.

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

* Drawing sheets: a lighter built-in border, the filing margin a choice (#3875)

Cameron's decisions after seeing the sheet in the app, 2026-10-23:
- No trimming marks in the built-in profile: the filled corner blocks
  take a lot of ink. trimming_mark_mm stays, so a profile can still ask
  for them.
- Centring marks stop at the drawing border: from the grid reference
  border to the drawing border, never into the drawing area (on A4 they
  crossed the tables). centring_mark_mm keeps its meaning, how far a
  mark reaches inside the drawing border, and now takes 0, the built-in
  value, as grid_border_mm already takes 0 for "on the drawing border".
  A mark of no length (a reach of 0 with no grid reference border) is
  refused.
- Margins of 10 mm all round. ISO 5457's 20 mm filing margin on the
  left, for drawings that get bound, is a named choice: filing_margin,
  off when not given. On, it stands in for margins.left_mm, which is
  kept, and still checked, for when it's off again. The 20 mm is
  FILING_MARGIN_MM, and validate() and layout() both take the margins
  from one helper, laid_out_margins().

A3 landscape is now 400 x 277 at (10, 10), with eight 50 mm columns.
With the filing margin it is 390 x 277 at (20, 10) again, the same as a
20 mm left margin set by hand. A profile with all three choices on
(ISO 5457's full border) lays out exactly as the built-in profile did
before, and the tests keep the old hand values for it.

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

---------

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

* Drawing W23a workshop plate oracle fixtures (#4950) (#4962)

* test(oracle): reviewer-aligned parameterized workshop plate fixtures (#4950)

Independent analytic W23 expected values and seven verified models,
including four R10 corners and the 2 mm tangent top chamfer.
Document accepted Drawing states and commit regenerated docs/oracle.md
from successful workflow run #29 (90/90 pass).

* fix(oracle): correct W23 chamfer centroid and clarify drawing session ownership

* Paper scene: plain records of what is drawn on a sheet (#4890 A1) (#5513)

geom::paper holds what a sheet shows as plain data: groups of strokes and fills, each group tagged
with the object that drew it (an opaque id) and its kind, with one validation pass over a bounded
record count. Seg2's checks and the coordinate rule move into profile.rs so section and paper share
them, and drawing_sheet takes its side and line limits from paper, so each limit is written once.

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

* Drawing sheets: centring marks across the whole margin (#3875) (#5554)

The built-in sheet's centring marks now run from the trimmed sheet's edge to the drawing border.
From the grid reference border, as before, a mark was the same length as a grid field mark, and on
A3 it sat on one (the sheet's centre falls on a field edge), so it no longer read as a mark once the
grid reference border's outline is left out (Cameron, after the prototype on #3875).

- DrawingBorderProfile.centring_mark_start: sheet_edge (the built-in's, and when not given) or
  grid_border (ISO 5457's figure 5); ISO's full border in the tests asks for grid_border.
- A mark still needs a length: one that stops on the drawing border needs room outside it on the
  side it starts from, every margin for sheet_edge, the grid reference border for grid_border.

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

* Drawing sheets: no centring or trimming mark longer than the largest sheet's side (#3875) (#6410)

A border profile capped every length at 5000 mm (the largest sheet's side) but two: a
centring mark's reach inside the drawing border and a trimming mark's length. A file could
give either a huge whole number, such as 18446744073709551615. The read's number check passes
whole numbers, so the profile took it as 1.8e19 mm, and saving wrote it as a decimal the next
read refused: a drawing that saved but didn't open again. K2's fuzz test of drawing files
(#3890) found it.

Both now take the cap the margins have, `MAX_SIDE_MM`, in `validate`, so the file is refused
when it's read. Layout's own checks against the sheet are unchanged, so a 5000 mm mark still
validates and is refused only on a sheet too small for it.

Tests: the hostile-profile test refuses 5000.000001 mm and 1.8e19 mm for each mark, and takes
5000 mm on its own; 4 planted faults (each cap removed or made one short) are all caught.

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

* Bill of materials types: phantom, reference, purchased and inseparable (#3874) (#5107)

* Bill of materials types: phantom, reference, purchased and inseparable (#3874)

A component's bill of materials type is a part property (`bom_type` in its metadata, set with
component.metadata.set and read by component.metadata, as part_number is), so older releases
keep it and no format change is needed. It isn't a design property: the Design Properties
dialog doesn't list it and document.metadata.set refuses it. A word this build doesn't know
reads as normal and is kept.

assembly.bom counts by type: a phantom has no row and what it holds counts where it is placed,
in the same rows as the same parts there; a reference counts nowhere, and nothing inside it
does; purchased and inseparable components are one row, and in parts only an inseparable
component's purchased parts are listed too. Rows of one part number but different types are
different rows. Rows and children carry bom_type. With no types set the rows are as before.

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

* Glossary: bill of materials and bill of materials type (#3874)

The bill of materials types are a new idea, so the glossary gets their
term, with the five types, and "normal" gets its fourth meaning in the
table of words with more than one meaning.

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

* Bill of materials types: a plain line for each (#3874)

The guide's table and the glossary entry say what each type is for in
plain words: something you make, something just to group, something to
use as a reference, something you buy, something made as one piece.
The names stay the five standard ones.

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

* Bill of materials: kits fold together only when their contents' types match (#3874)

With group_children, two kits with one part number share a row when what they hold reads the
same. That reading (kit_key) compared each child by part number and count, but not by its bill
of materials type, though rows themselves are told apart by type. So a kit holding a made bolt
and a kit holding a bought one, both KIT, folded into one KIT x2 row listing the first kit's
bolt twice: the other type was lost, and which one depended on creation order. 0xlillith found
it in review of #5107.

kit_key now includes each child's type. Equal part number, type and contents still fold.

Test: two KIT kits each holding a BOLT fold into one row of two; once one bolt is purchased they
are two rows, each listing its own bolt. Planted fault (the type left out of the key): caught.

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

---------

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

* Drawing files: a drawing saved as a container or as plain JSON, never read as a design (#3890) (#7098)

* File format families: drawings share the design format's versioning (#3890)

The format code works for a family of files: its own tag, its own numbers and
its own folder of change files, with one reading of a file's format, one
refusal of a file a newer build saved and one upgrade for every family. Designs
(solidcraft/N) are one family and keep everything they had: FORMAT and FORMATS
are as they were, and pending_names, format_string, version_of and read mean
what they did. Drawings (solidcraft-drawing/N) are the other, with format 1 as
their first record and each later change one file in
crates/doc/src/drawing/format/pending/, as design changes are. Nothing saved
changes.

A design tag never reads as a drawing's or the other way round, and a file that
names no format is a design from before format strings, never a drawing. Every
refusal names its family; a design's read word for word as before. A design
listing a pending change this build doesn't have is now refused as a newer
build's before its content is checked, as a newer number already was, so a
newer build's fields never read as damage. The list reader is
take_names(v, key, noun), the shape #4811 uses for its own list.

For the maintainer, the release step changes: scripts/fold-format-pending.sh
now folds every family that crates/doc/build.rs lists (FAMILIES), each into
its own next number: designs into crates/doc/src/format/v<N>/ as before,
drawings into crates/doc/src/drawing/format/v<N>/ from 2. With nothing pending
in a family it says so and moves nothing. Its test folds a drawing change into
v2 beside the design changes. docs/releasing.md, CONTRIBUTING.md and AGENTS.md say so.

build.rs lists each family's changes from its own folder, each folder's
versions contiguous from its own base.

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

* Drawing record: format 1 of the drawing family (#3890)

A drawing is now a record of its own (crates/doc/src/drawing.rs): its name,
the designs it draws (sources: an id and the design file's path, relative to
the drawing's folder as a linked design's is), its properties under the design
metadata's rules, its units (each design's own, or one length unit of the
expression unit table), its content language (a BCP 47 tag) and its sheets.
A sheet holds its paper, a copy of its drawing border's profile, the
resolution its pictures are rendered at (300 dpi unless it says, 25 to 2400),
the component it documents and whether it is left out of the sheet count or
of printing. This is format 1 of the drawing family; each later kind of
record adds one file in crates/doc/src/drawing/format/pending/.

The rules: one id counter for every object, raised above the largest id when
a file is read and never rewound; every reference is an id that must resolve,
or the file is damaged and refused, never patched; 1 to 1,000 sheets, at most
64 sources, names of 1 to 256 bytes, paths of 1 to 4,096 bytes without NUL.
The same drawing always saves as the same bytes.

Drawing::from_json reads at most 16 MiB of text, then runs the family's format
check, the number check, the upgrade, the typed record (unknown fields refused
everywhere) and Drawing::validate. The number check is now one function every
family calls, Family::check_numbers, told which field holds other tools' data;
a design's refusals read as before.

Nothing saves or opens a drawing yet: the container is the next step.

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

* Drawing ids: random and collision-free from format 1 (#3890)

A drawing's objects now take random ids, the form jyviko chose on #3890 from
#1017: no counter, so objects added on two branches never share an id and
never edit one line. The allocator is one module for drawings now and designs
later (#626 Phase 3), crates/doc/src/ids.rs: an IdSource gives nonzero u64s by
splitmix64 from a seed, each checked against the ids in use and drawn again
when it is one, or when it is 0. A run never draws a number twice, so at most
one draw per id in use comes before a free one, and a seeded run over a file
the same seed made still gets a new id. IdSource::seeded gives the same ids
for tests and replays; IdSource::from_entropy seeds a session from the
standard library's hash keys, the time and a count, with no new dependency.

In a file an id is 13 characters of Crockford base32, lowercase
(0000000000001 to fzzzzzzzzzzzz): ids::encode writes it and ids::decode reads
only that spelling, case-sensitive, so an id has one form to search for.
Anything else, 0 included, makes the drawing damaged. The drawing loses
next_id (a file that holds one is refused), and Drawing::new and allocate_id
take the IdSource. Messages name ids as the file writes them. The component a
sheet documents stays its design's own number until designs move over.

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

* Drawing record: a drawing too large to reopen isn't saved (#3890)

from_json reads at most MAX_DRAWING_BYTES (16 MiB), but to_json had no limit. Other tools'
properties are kept as they came, and indenting them can grow a drawing read in at about 4 MB
past 16 MiB (two million zeros in one array), so the next read refused what the save had just
written. 0xlillith found it in review of #3891.

to_json now returns a Result and refuses JSON longer than a read takes ("drawing too large to
save (at most 16 MiB)"), so every drawing saved opens again. Nothing writes drawing files yet, so
the error reaches the future file writer before it publishes anything.

Test: that case is refused, and a smaller compact drawing saves and reopens unchanged.

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

* Drawing files: a drawing saved as a container or as plain JSON, never read as a design (#3890)

K3 of the drawing storage plan (W11.2): the file a drawing is saved in. Nothing writes one from a
command yet; opening and saving come with K5.

- A drawing container is a design container holding `drawing.json` (Deflate, as `document.json`)
  in place of `document.json`, and `container.json` says so:
  {"content": "drawing", "format": "solidcraft-container/1"}. A design's `container.json` is as it
  was, with no `content`. The plain JSON form is the drawing's own text. Both give the same bytes
  every save, so a drawing can live in version control as text.
- `io::file_kind` tells a design from a drawing by `container.json` or by the JSON's format. The
  design reader refuses a drawing ("this file is a SolidCraft drawing, not a design: open it as a
  drawing") and the drawing reader a design; plain JSON only asks when it isn't a design, so
  reading a design costs nothing more. A container's content this build doesn't know, or a drawing
  container that also holds a design's `document.json`, is refused.
- `read_drawing` (bytes, either form), `read_drawing_file` (one seekable handle, the design
  reader's checks) and `write_drawing_container`; `DRAWING_EXT` (`scdwg`) and
  `is_drawing_path` beside the design's.
- The container writer's set-up and finish, the file opening and the container sniffing move into
  one function each, which designs and drawings both use. The container's own checks (budgets,
  entries, the ZIP directory) are the design container's, word for word.

Tests: a drawing round-trips through both forms byte for byte, and the container holds exactly
`container.json` and `drawing.json`; `file_kind` on both forms of both kinds; each reader refuses
the other's file; unknown content, a stray `document.json`, a missing drawing, an unknown
container version and a damaged drawing refused; both forms from disk, and a directory refused;
mutation fuzz of both forms (an error or a drawing, never a panic).

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

---------

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

* Paper scene: SVG of a sheet, its lines, arcs, cubics and fills (#4890 A3a) (#7032)

The first export of a paper scene, as SVG first (jyviko and 0xlillith on #4890), from the research
in the plan's §16. A3b adds ellipses, hyperbolas and Béziers above degree 3, which this refuses as
"not supported yet".

- `geom::paper::Prepared`: writers read a scene through `Scene::prepare`, which checks it first, so
  none reads one unchecked. Clipping goes there too, once (A8).
- `geom::paper::arc_pieces`: an arc's ends in pieces of at most a quarter turn, the last its own end.
  A half turn drawn from its ends is ill-conditioned: rounded to the nanometre (six decimals of a millimetre) its
  centre moves up to 1e-2 mm, where a quarter turn's stays under 1e-6 mm.
- `io::paper::paper_svg`: one user unit a millimetre, the size in mm on the root; y flipped by
  writing the height less y, so the file's numbers are page coordinates and an anticlockwise arc
  has sweep flag 0; lines `L`, arcs `A` (never large-arc), cubics `C`, parabolas `Q`; fills
  black and even-odd; one path per item; groups as `data-object` and `data-kind` (object ids can't
  be XML ids). No background, so cutters see only the marks.
- Numbers are whole nanometres (six decimals of a millimetre): no exponent, no "-0", the same bytes every time. They are the
  writer's own state too, so a joint is written once and a closed path ends exactly on its first
  point. Numbers are written straight onto the document, with no text made per number. A width
  under half a nanometre is written as one, and a piece too short for the grid is merged into the
  next, both counted in the report; a path's last piece has no next, so it is kept however short
  (unless it has no length). Output over 64 MiB is refused.

Tests: the A3 drawing border's whole document; quarter, 270°, clockwise and whole-turn arcs, and a
circle of two halves giving the same four quarters; the quarter-turn boundary both sides; cubics and
parabolas, and the conics refused; the number format; a closed path whose start and end round
apart; a sliver merged; an open path's short last line and arc kept, and one of no length dropped; read back through our own SVG reader (lines exactly, the arc as an arc);
the scene's JSON mutated into the writer (never a panic).

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

* Drawing W17a: view definitions as data (#4816) (#4817)

* claim: #4816

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

* Drawing views as data: named views, scales and the map onto paper (#4816, W17a P1)

What a drawing view is, before anything draws it (geom::drawing_view):
- NamedView: the six principal views and the isometric views from the four upper corners, each
  an exact basis (a Plane whose normal points back at the viewer), never taken from a camera,
  whose pitch clamp would tilt a top view, with no -0 to write out; `iso` reads as the
  front-right one.
- basis_looking: any other view from its direction, an up and an origin (paper x = direction x up,
  paper y = x x direction), refusing a zero direction, an up along it, or an origin that isn't
  finite or within 1e12 mm. W07's
  {direction, up, origin} view reads through it.
- Scale: paper : model as two positive numbers, read and written as "1:2", from 1:100000 to
  1000:1; Scale::fit picks the largest ISO 5455 preset (50:1 to 1:10000) at which a view fits a
  space (W07's "fits the page" default), compared as size x paper <= room x model so an exact
  fit at a reduction isn't lost to rounding.
- ViewFrame: a view's basis, scale and place on paper, the point of its plane at `centre`
  landing at `position_mm`. Its paper map moves the view's curves and their accuracy onto
  paper (paper error = scale x model error); `boundary` maps a box of its plane onto paper from
  its four corners.

In geom, where Seg2 lives:
- Similarity2 and Seg2::similar (new): scale, turn and shift a segment exactly, an arc staying an
  arc. Similarity2::new refuses a scale that isn't above 0 and works the turn out once, not per
  point;
- Seg2::map_controls: a segment's control points mapped by any function. It was doc's
  sheet::map_controls; its four callers in doc use the method now, unchanged.

CurveAccuracy::scaled gives a curve's accuracy at another scale.

Tests: each named view's basis and viewer side, and the same bases from a direction and up; a 50 mm cube in every principal view at 1:1 and
1:2, and in isometric (edges 50 x sqrt(2/3), outline 50 x sqrt(2)); the view's centre on its
position (W07's 100 x 20 front view on A3 at 2:1); scales read, written and refused; the fitted
scale, an exact fit at 1:5 and ISO's smallest; no -0 in any named view's axes; an origin refused by
its own message; hostile bases, centres and positions; every segment kind moved by a similarity,
and the similarities refused; scaled accuracy.

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

* Drawing views: the bottom view pinned, square near parallel, bounded scales and boxes (#4816)

From DevonHope's review of #4817:
- The tests pin all six principal views exactly. The direction-and-up test uses literal ups,
  so a bottom view turned half round now fails it.
- basis_looking squares x to the direction before normalising. Near the 1e-9 threshold the
  normal was up to 8.8e-9 rad off; now it is about 1e-16.
- Scale::fit fits nothing for a size or room that is negative or not finite. A size of 0 still
  sets no limit that way.
- Each side of a scale is 1e-6 to 1e6, so a scale writes at most 49 characters.
- ViewFrame::boundary returns a Result. It refuses a box that is inside out or not finite on
  paper, instead of dropping a corner.
- paper_map, boundary and seg_to_paper say which coordinates they take.

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

---------

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

* Drawing W18 S2: title blocks as data (#3875) (#3877)

* Title blocks: what a cell shows and where its text goes (#3875)

geom::cell holds a title-block cell's content: parts of text and fields
(component, drawing, sheet, revision or prompt, by key, with an optional
unit and decimals kept as written for the resolver to check), at most 64
parts and 4096 bytes of text, with no control character but a line break.
Beside it are a resolved value (live, overridden and drifted, or missing,
with "-" for each part that couldn't be read and why), an override with
the value it replaced, and the projection a drawing's views use (first or
third angle), which the title block's symbol will read.

A fit warns and never cuts: a count of characters, not bytes, with its own
count for text in Chinese, Japanese, Korean or Yi characters.

The cell text rule places a label and a value: the label's baseline 1 mm
plus its cap height below the cell's top, the value box under it inset by
1 mm, one line centred in the box, and wrapped or several lines down from
its top at the font's pitch, with a warning when the last falls below the
box. Wrapping by width, width warnings and glyphs a font lacks need the
text measured, so they wait for the paper scene's text.

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

* Title blocks: definitions and their place on a sheet (#3875)

geom::title_block holds a title block's definition: its size (1 to 1000
mm a side), its outline and cell line widths, and up to 64 cells. Each cell
has a snake-case name of its own, a rectangle measured from the block's
lower-right corner, an optional one-line label with its cap height, and a
value: text from cell content, with its cap height, alignment and fit, or
the projection symbol, which reads the drawing's projection and is never
fixed in the block.

validate() checks a definition on its own, as when one is read: cells
inside the block and not overlapping (touching is fine), label and value
cap heights that fit, and the content's own limits. layout() puts the
block on the drawing border's lower-right corner of a sheet, refusing one
that doesn't fit, and gives each cell's rectangle, its label's place and
its value box by the cell text rule. On A3 landscape a 180 x 54 mm block
runs from (230, 10) to (410, 64); it fits A5 landscape and every larger
sheet, but not A5 upright.

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

* Cell text: a value box lies within the paper's coordinates (#3875)

place_lines took any finite, ordered value box, but centring one far out (min x 1e308, max x
1.1e308) overflows: the anchor came back infinite, with no warning. 0xlillith found it in review
of #3877.

The paper scene already bounds every coordinate it holds by MAX_COORDINATE_MM (twenty times
the longest sheet side). That limit is now public, and place_lines keeps its box inside it, so
its anchor is always finite: one rule for what a paper coordinate may be.

Tests: the hostile-box test refuses both overflow cases from the review and a box just past the
limit on each side, and places lines in a box the size of the whole paper limit. The fuzz test's
numbers now include values past the limit (1.1e5, ±1e308).

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

* Title blocks: padding from the lines' inner edges, one label rule, nothing unread (#3875)

A cell's text is padded from the inner edge of its lines: each side of a cell moves in by half
the line drawn on it (the outline's on the block's edges, a cell line's inside), so a wide
outline never covers the text. Before, the padding ran from the line's centre.

The label's one-line rule lives with the cell text rules (`cell::check_label`), where parts
lists will find it. The cell types nothing read yet (`Override`, `Resolved`, `State`,
`Problem`, `Reason` and `MISSING`) wait for the resolver that will read them.

The tests check the exact thresholds inside the lines (a label that leaves no room, a value
that just fits), pick each of the lines, alignment and wrapping on their own so every
combination comes up, and lay fuzzed blocks out on every standard sheet, either way, with and
without the filing margin.

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

* Title blocks: the drawing border's line, descenders, room where the block sits (#3875)

From DevonHope's review of #3877:
- Each cell's lines are clamped to the inner edge of the drawing border's line. That line runs
  along the block's right and bottom edges, and along any other edge it reaches, as when the
  block fills the border. Aabb2::intersection, the 2D twin of Aabb3's, does the clamping.
- layout checks each cell's room again where the block sits (place_cells, which validate runs
  at the origin), so rounding at the real corner can't turn a cell inside out.
- The cell text rule counts descenders, at 3/10 of the cap height (ISO 3098's lettering B).
  They may reach into the padding under a value box, never past it to a line. place_lines
  warns Height when they would, and validate asks it whether one line fits.
- The module doc no longer links to a private function.

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

---------

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

* Drawing W06a: exact orthographic hidden lines for the analytic subset (#2455) (#2457)

* Drawing W06a: exact orthographic hidden lines for the analytic subset (#2455)

crates/geom/src/hlr (L0): prepare once per body, then hidden_lines for a
scene of placed bodies. Edges and face silhouettes are projected from their
exact curves, split where projections cross, and each piece is classified by
rays toward the viewer against the trimmed faces: visible, hidden or
explicitly uncertain, never guessed. Planes, cylinders, cones, spheres and
tori are supported whether stored as analytic records, revolutions or spline
patches; freeform faces are reported and make what they could hide
uncertain. Screen geometry is Seg2 with the W01a accuracy vocabulary: exact
lines and conic arcs where the projection is exact, bounded chords
otherwise. docs/drawing-hlr.md holds the contract and its limits.

* geom/hlr: handle the brep records main gained in #3093

Main's exact model storage (#3093) added Curve::SurfaceIntersection,
Surface::PlaneThrough and Surface::PlacedRevolution, and the HLR matches on
the records no longer compiled. A plane through three points is a plane: it
is trimmed and intersected as one, and a box stored that way draws the same
bits as one with axis-stored planes. The procedural intersection curve and
the placed revolution (whose affine placement may scale it unevenly) are
refused by prepare as not supported yet, until the kernel adapter handles
them. The records' positions move with the body's centre like the others.

* geom/hlr: pad plane faces' boxes, so a plane record off by rounding still hides

Plane faces took the box of their boundary chords unpadded, flat along the
normal. When the plane record and its edges disagree by rounding (a unit cube's
top plane one ulp above its edges, a 100 mm cube's 1e-11), the ray toward the
viewer reached the plane just outside that box, the slab test culled the hit,
and the back edges came out Visible with no doubt. Every face's box is now
padded by 1e-6 of its size, as curved faces' boxes already were. Reported by
DevonHope on #2457.

* geom/hlr: limits from review: trim samples, scene extent, crossing pairs, surface groups

From DevonHope's review of #2457:
- Options::trim_samples must be 1 to MAX_TRIM_SAMPLES (4096).
- Placed bodies must stay within MAX_COORDINATE (1e12 mm, as section and brep
  bound stored numbers). Placements 1e200 apart made the scene's size infinite
  and every tolerance taken from it with it, so everything came out Visible.
- The crossing search counts its candidate pairs before it starts and meets
  each pair only in the first grid cell the two segments share, instead of
  keeping a set of the pairs met, which could outgrow wasm's 4 GB before the
  500 million pair budget tripped. The same pairs in the same order.
- prepare groups faces on one quadric or torus by comparing each face with
  the faces of its kind whose size agrees to the tolerance (sorted), not with
  every face; at most 50 million comparisons. prepare_cancellable polls a
  cancel callback between edges, faces and comparisons.

* geom/hlr: boundary chords on a grid, and no fine pass over coincident curved chords

The cost items of DevonHope's review of #2457:
- Every ray hit asked how far it lies from its face's boundary by scanning all
  of the face's chords (4096 per circle; a plate's face with 256 holes has
  about a million). The chords are now bucketed on a uniform grid over the
  boundary's box, cells at least as long as the longest chord, and the test
  looks only at the cells around the point. It finds exactly the chords the
  scan finds.
- Chords of two curved curves that lie on each other end to end (aligned holes
  seen along their axes) were cut into 16 by 16 pieces and intersected, to
  find crossings that are only noise within the curves' tolerance. They are
  now skipped; visibility changes where a coincident run starts or ends, and
  chords that only partly overlap still find those.
- Fixtures: a plate with holes, coaxial cylinders seen along their axis, and
  300 placements.

* geom/hlr: pad boxes by the body's coordinates too; coincident chords only to rounding

Review of the previous commits found two holes:
- The padding grew with the face's size, but rounding grows with where the body
  was modelled: a 0.001 mm cube 1e7 mm out, its top plane one step (1.9e-9 mm)
  above its edges, still had its hit culled. The pad now adds 1e-12 of the
  body's offset.
- Two chords were taken for coincident when their ends lay within each other's
  sag, which in a coarse scene can hide real crossings: an arc can bulge across
  a line 1 mm off its chord twice. Only chords that coincide to rounding are
  skipped now; identical projections, the costly case, still are.

* geom/hlr: coincident curves meeting at shared sample points do not cross

Timing the review's scenes showed that skipping coincident chords was not
enough: chords of two coincident curves also touch end to end at every sample
point they share, and each touch was a crossing to refine and split at. 64
coaxial cylinders seen along their axis gave 8.3 million crossings (19 s), 64
cylinders one behind another along the line of sight 4.2 million (214 s;
before these commits the crossing budget refused it). A touch at a shared
sample point is now dropped when the two curves also coincide, to rounding, on
both sides of it. Where a coincident run starts or ends one side differs, and
the touch is kept.

* geom/hlr: the own-face margin allows for rounding where a body was modelled

The far 0.001 mm cube of the previous commit showed a second effect of records
rounded at large coordinates: its top plane, one ulp (1.9e-9 mm) above its
edges, hid the top edges themselves, because a ray leaving an edge met its own
face 3.2e-9 mm along, past the own-face margin of 1e-6 of the face (1.4e-9).
The margin now also covers 1e-14 of the body's offset (about 45 ulps of its
coordinates), which only matters for parts far smaller than their distance
from the origin.

* geom/hlr: no parity test for a hit on the boundary, where it is uncertain anyway

* docs/drawing-hlr: the fixtures of the review's second round

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>

---------

Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Benny1496 <163034806+Benny1496@users.noreply.github.com>
Co-authored-by: Mateusz Sych <151755914+SychPL@users.noreply.github.com>
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-28 13:31:16 +00:00
Ahmad Khan 6af571eb40 Flat Pattern: show the whole flat size (#7495) (#7496)
* claim: #7495

* Flat Pattern: show the whole flat size (#7495)

Each side of the flat size and the thickness get their own row (Flat size X, Flat size Y,
Thickness), so the panel's width no longer cuts the value off after its first side.

* Flat Pattern: the flat size labels in Arabic, Hebrew and Vietnamese (#7495)
2026-10-28 13:25:09 +00:00
ermanyop-stack a5f472c23e Prevent angular drags from collapsing construction geometry (#8148) (#8160)
* Reject newly collapsed angular reference lines (#8148)

Reject angular solutions that collapse a reference relative to its input length. Cover constrained drags and already-short references. Engine replay and full CI validation remain in progress.

* Cover live and non-live angular drags (#8148)

Adds a replay of #8148 for live and non-live drags. The integration test compiles locally, but the Windows GNU test executable exits with STATUS_ACCESS_VIOLATION before running tests, including with --list/--help. Kept in the draft PR pending validation on a working native runner.
2026-10-28 12:44:16 +00:00
semmelrinde 34efb90489 Romanian: translate 946 messages the catalog was missing (batch 1) (#8193)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:44:10 +00:00
Tarik Sijerčić f060702676 i18n: add Bosnian, Croatian and Serbian interface languages (#7813)
* i18n: add Bosnian (bs), Croatian (hr) and Serbian (sr) catalogs

Full translations of all English messages, the pending ones included,
kept in step with one shared terminology:

- Bosnian (Bosanski): ijekavian, Latin script.
- Croatian (Hrvatski): ijekavian, Latin script, Croatian standard terms
  (točka, kut, brid, ploha, značajka, spremi, izbornik, sučelje).
- Serbian (Српски): ekavian, Cyrillic script; brand and file names,
  acronyms, units, key names and placeholders stay in Latin.

The Browser panel is "Stablo modela" so it is never confused with a web
browser, and the History panel is the timeline ("Vremenska traka"), not
the undo history, per the glossary.

Registers the three catalogs, adds them to the new-language test, a
pending attribution file, and a terminology note in docs/localization.md.

* i18n(bs, hr, sr): translate the 195 messages added on main since the catalogs were made

Covers the navigation bar customisation, the render stage, the new
command help texts and the Assistant start-up preferences, so the three
catalogs again carry every English message.
2026-10-28 12:38:27 +00:00
Homero Thompson d6b1d03695 Browser: an exact body name wins over a case-only sketch match (#6967) (#7004)
* claim: #6967

* Browser: an exact body name wins over a case-only sketch match (#6967)
2026-10-28 12:22:27 +00:00
Mateusz Sych e181015fd1 MCP save: a folder the agent opened or saved a design in stays in scope (#7181) (#7216)
* claim: #7181

* MCP save: a folder the agent opened or saved a design in stays in scope (#7181)

The agent's default folders held the open design's folder only while that
design stayed open there, so after a new design, an opened STEP file or a
save-as elsewhere a save beside the earlier design was refused, while the
same save through the control port worked.

The MCP server now keeps, for the rest of its run, the folder of every
design file a call opened or saved (open, save, and doc.open, file.save,
file.save_as through execute or batch, path given in full) and, headless,
of every design open in its session. Such a call only succeeded because the
folder was already in scope, so naming a path adds nothing. The folders get
the open design folder's terms (inside home or temp, not hidden, hidden
entries unreachable), at most 16. Bridged, they go to the app as
design_folders, which it judges the same way.

The save tool description and docs/mcp.md say how to add a folder with
--allow-path.

* MCP scope: a design's folder is the folder its file resolves to (#7181)

A link in another folder to a design inside the agent's folders passes the
scope check, which follows it, but its own folder was what got remembered
(and, for the open design, what counted while it was open). Take the folder
of the file as the check resolves it.

* Control test: a hidden file the export format accepts (#7181)

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-28 12:22:20 +00:00
Homero Thompson 387e86729c Sketch: the rounded rectangle completes from a vertex like the plain one (#7878) (#7943)
* claim: #7878

* Sketch: the rounded rectangle completes from a vertex start point (#7878)
2026-10-28 12:20:46 +00:00
Dahak Builds 64ff25f380 Fix sketch drag breaking midpoint-constrained line solver (#7635) (#7810)
* claim: #7635

* Keep the drag cursor on the manifold of an ill-conditioned sketch (#7635)

A construction line collapsed to zero length makes its angle row's entries
on that line ~1e7 while the same row's entries on the measured line are ~1.
Dragging an endpoint of the midpoint-pinned line then failed two ways: the
null-space rank analysis, which scaled rows only, hid the small (physically
meaningful) entries below its pivot tolerance and let the measured line
rotate freely; and the cursor's refinement, which demanded a TOL-tight
correction the ill-conditioned system cannot deliver, gave up entirely and
let the drag leave an unsolvable step placed - breaking the perpendicular
and angle constraints.

Equilibrate the null-space Jacobian on both sides (columns by their largest
entry, then rows) so one row's entries share a scale, and let the cursor
keep working from a pose the solver itself calls solved (residual below
ACCEPT) when refinement stalls above TOL, accepting corrected trials at the
same tolerance the final validation uses.

---------

Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
2026-10-28 12:20:40 +00:00
Jomguy2 1dba7f7f04 Agent chat: OpenCode Go preset with per-model endpoints (#6111)
* agent chat: OpenCode Go preset with per-model endpoints

One preset (https://opencode.ai/zen/go/v1) covers every Go model: each
model is routed to its endpoint family (chat completions, responses,
messages, Gemini), always with the x-opencode-session header Go
requires. Unknown models fall through protocols when Go names the
mismatch; the conversation and tools translate per protocol.

* agent chat: keep Go reasoning metadata, route all listed models (#5656)

* fix agent chat Go endpoints to documented inference routes

* style: collapse long assert in Go routing test (cargo fmt)

* refactor: share Go tool/args helpers, table-driven protocol routing (no behavior change)

* refactor: share GoCallAccum across Go reply parsers (no behavior change)

* refactor: share part/call extractors across Go history converters (no behavior change)

* refactor: share post_to envelope between post and go_post (no behavior change)

* refactor: pass system note into go_responses_input, drop history clone (no behavior change)

* refactor: lazy Gemini name map, shared go_tools, named tool-call cap (no behavior change)

* refactor: share shaped-tool wrapper across Go tool converters (no behavior change)

* fix: set request_timeout_s in Go test setups for rebased main (no behavior change)

* fix: expect ToolsAvailable first in tool-loop tests (matches new event order)

* simplify: reuse tool_arguments, drop restater comments (no behavior change)

---------

Co-authored-by: Julius <julius@localhost>
Co-authored-by: Jom_g <jom_g@users.noreply.github.com>
Co-authored-by: HoloPort <holoport@localhost>
2026-10-28 12:20:19 +00:00
N3MI-DG 7d9b28e20d Context menu: only the row under the pointer stays highlighted while a flyout closes (#8065) (#8066)
* claim: #8065

* Context menu: only the row under the pointer stays highlighted while a flyout closes (#8065)

A row that owns a flyout kept its highlight for the whole of the flyout's
closing delay, so the row the pointer had settled on lit up alongside it.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-28 12:19:51 +00:00
github-actions[bot] 48bcc83e2e Regenerate generated files and fold pending files [skip ci] 2026-10-28 14:56:01 +00:00
austin62jake 1e3cb3e36e Motion interference: a check cut short is not clear, and budget_s gives it time (#7855) (#7934)
* claim: #7855

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

* Motion interference: a check cut short by its work budget is not clear (#7855)

motion.interference stops when its work budget runs out (truncated) but
answered clear: collisions.is_empty(), so a check that reached only the first
steps read as a pass: a gear pair checked for 14 of 61 samples was "clear".
clear is now true only when every sample was checked and none collide.

The budget is an argument of interference_within, so a test can cut a check
short: B slides up to touching A, a full check is clear, and the least work
that checks a first step stops after 1 of 16 samples with clear: false (it
was clear: true before). The guide and the mechanisms skill say the same.

The issue's second request, a budget_s on motion.interference, is not here.

Refs #7855.

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

* Motion interference: budget_s gives a check the time it needs (#7855)

The work budget is fixed, so a gear pair drawn in detail was cut short
after a few samples, or refused before its first one. motion.interference
now takes budget_s (seconds, above 0, at most 86400, as inspect.interference
allows): the check then runs until that time instead of the work budget,
looked at before each sample, and echoes budget_s in its answer. Without
budget_s nothing changes. The refusal before a first step now names the
budget that ran out and suggests budget_s.

Tests: given a time, the smallest work budget (which checks no step) no
longer stops the check, through motion.interference too; budget_s 0, -1,
1e9 and "soon" are refused.

Fixes #7855.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 12:19:10 +00:00
Inguss 4d6664e4ca fix(ui): expose Redefine Sketch Plane in History menus (#8024) 2026-10-28 12:18:53 +00:00
raykholo d56dc310b0 Browser: Collapse All keeps the clicked row open (#8032) (#8033)
Collapse All from a component or placement row folds everything under that
row and leaves the row itself open, matching the whole-tree Collapse All,
which already keeps the design row open.
2026-10-28 12:18:28 +00:00
semmelrinde 4ab612ea67 Standard Parts and Contact Set: label the rows in words, not with raw keys (#8005)
* Standard Parts and Contact Set: label the rows in words, not with raw keys

The Insert Part dialog listed a size's dimensions under the library's
keys (d, head height, socket depth), and the Contact Set dialog's
checkbox read "enabled". The dimensions now take messages (Nominal
diameter, Head height, Socket depth, ...) and the checkbox reads
Enabled, translated like the rest of the dialogs.

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

* Contact Set: translate the Enabled checkbox's label

ui_kit::checkbox paints the text it is given; the callers translate it,
as Configuration Rules' Enabled does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:17:07 +00:00
semmelrinde 73f06753b6 Swedish: translate 946 messages the catalog was missing (batch 1) (#8181)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:16:54 +00:00
semmelrinde 292177e085 Preferences: the tab colour strip names the tabs as the ribbon does (#8027)
The Interface page's strip of tab accent colours looked up its own
word-cased names ("Sheet Metal", "Surface"), which the catalogs
mostly don't carry or carry with another meaning: in German it read
"Sheet Metal" and "Flächenkörper" where the ribbon says Blech and
Fläche. It now names each tab the way the ribbon does, through the
tab's heading message, and the Form tab gets one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:16:48 +00:00
Colin Faulkingham d7111c7326 Ribbon: keep workspace captions inside the switcher (#7931) (#7956)
* claim: #7931

* Fix translated workspace captions overflowing the switcher (#7931)
2026-10-28 12:16:19 +00:00
github-actions[bot] 745184f2fc Regenerate generated files and fold pending files [skip ci] 2026-10-28 14:26:54 +00:00
semmelrinde 942f11169c Preferences: the snap-suppress choice and Direct interaction in the user's language (#8067)
* Preferences: the snap-suppress choice and Direct interaction in the user's language

The Hold to stop snapping choice joined its keys with an English "or"
("Cmd or Option", the default) in every language, and Navigation's
Direct interaction setting and its note had no message. The choice now
reads through a message with the keys as placeholders, and the setting
gets its two messages.

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

* Move the new test away from where another open PR adds one

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:48 +00:00
semmelrinde 029cf29b49 Dialogs: 54 labels that read English in every language get messages (#8049)
* Dialogs: 54 labels that read English in every language get messages

The construction dialogs' selection fields (First edge, Second point,
Through point, ...), the joint types (Rigid, Revolute, Slider, ...), the
Sheet Metal and Plastic rules' rows (K Factor, Flange Gap, Min Draft,
...) and a few options and prompts are passed to the label translator as
English source text that no catalog carried, so they showed English in
every language. They are added in one pending file, with the eleven
usual translations, and a test makes sure every dialog's selection
fields keep a message.

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

* Dialog labels: German Flange Gap is Flanschspalt, as the catalog's Flansch

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:35 +00:00
semmelrinde 7ad059aec9 Italian: translate 719 messages the catalog was missing (batch 1, all but the Render workspace) (#8062)
The Italian catalog lacked 1350 of the English messages. This batch
translates the 719 outside the Render workspace: Preferences, the
Assistant, the help texts, command labels, materials, navigation and
the rest. It uses the catalog's own terms (Schizzo, Lavorazione, Corpo,
Giunto, Cronologia, Struttura, Risalto, Appoggio, Aggancio a scatto).
The Render workspace's 600 messages are left for a second batch while
that workspace is still being built.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:24 +00:00
github-actions[bot] a2a00aa9ee Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:58:14 +00:00
github-actions[bot] 59551c59ab Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:49:47 +00:00
semmelrinde bf1b09698c Insert Part: the part families and the Name hint in the user's language (#8018)
* Insert Part: name the part families in the user's language

The Type list of Insert Part and the Change Part Size dialog showed the
library's English family names (Socket head cap screw, Hex nut, ...) in
every language. Each family now has a message under its library id,
and a family the catalogs don't know keeps the library's name.

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

* Insert Part: the Name field's automatic hint in the user's language

The hint was the English word "automatic"; it now takes the catalogs'
parts.automatic_name, which every language already translates.

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

* Move the family-name helper and its test away from where #8005 adds its own

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:15 +00:00
semmelrinde fc2142b407 Turkish: translate the 200 messages the catalog was missing (#8070)
* Turkish: translate the 113 messages the catalog was missing

The Turkish catalog lacked 113 of the English messages: the newer
Preferences settings, the hole presets, the slicer messages, the
Assistant's start-up options and tool summaries, and the Render
workspace's stage, shots and inspector labels. They are translated in
one pending file, with the catalog's own terms.

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

* Translate the 87 messages main added since (the navigation bar's)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:15:03 +00:00
semmelrinde d3b2c715d3 Dialogs: translate the texts a few dialogs draw themselves (#8056)
* Dialogs: translate the texts a few dialogs draw themselves

The joints' Play/Stop buttons, the Motion Study's Loop and the BOM's
sub-parts checkboxes, the Stitch tolerance hint, the Flange type and the
Emboss wrap note were drawn as English literals; the joint lists named
each joint's type by its Rust name (PinSlot, PointOnPlane). They now take
messages, and the joint lists use the Type box's label, translated.

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

* Move the new test away from where another open PR adds one

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:51 +00:00
semmelrinde 09308fceb1 Dialogs: 96 hints and refusals that read English in every language get messages (#8083)
The Surface, Sheet Metal, Plastic, Frame, Move, Align, Assembly, Cam,
Coupling, Sweep, Part Size and sketch editing dialogs refuse their
inputs with English sentences ("select bodies first", "pick faces of
one body"). The dialog translates a refusal through the catalogs as it
shows it, but no catalog carried these 96, so they showed English in
every language. They are added in one pending file with the eleven
usual translations.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:41 +00:00
semmelrinde 8f87509f7d Russian: translate 841 messages the catalog was missing (batch 1) (#8077)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:34 +00:00
semmelrinde c962e5e8cd Traditional Chinese: translate 897 messages the catalog was missing (batch 1) (#8078)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:33 +00:00
semmelrinde 7df858594d Indonesian: translate 898 messages the catalog was missing (batch 1) (#8098)
Everything outside the Render workspace, which is still being built:
Preferences and the new navigation bar, the Assistant, the help texts,
command labels, materials, hole presets, the slicer messages and the
remaining dialog labels, with the catalog's own terms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:22 +00:00
semmelrinde fed9605357 Sketch: the status in the view and the browser's editing mark in the user's language (#8084)
* Sketch: the status in the view and the browser's editing mark in the user's language

While a sketch is edited, the view's corner said "Sketch1 — 2 degrees of
freedom" (or "fully constrained", "constraints conflict") and the
browser marked the sketch "(editing)" or "(attachment lost)", in
English whatever the language; one degree of freedom read "1 degrees of
freedom". They now take messages: the catalogs' existing degrees-of-
freedom ones and four new ones.

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

* Sketch status and editing mark: the sketch's default name as shown elsewhere

The browser shows a default name in the user's language ("Skizze 1")
except on the edited sketch's row, whose mark made it a different text;
the view's status kept the stored name too. Both now use the name as the
browser shows it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:38 +00:00
semmelrinde bf1886e90a Motion dialogs: translate the notes and refusals they draw themselves (#8096)
Motion Study, Motion Trace, the motion plot, Explode and the contact
check drew their notes and refusals as they got them ("add a key for a
joint first", "select a point on a component", "none"), so they read
English in every language. They now go through the label translator, and
the four that no catalog carried get messages.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:38 +00:00
phanoob dfa9afd39f Sketch: leaving a sketch opened inside a component gives the active component back (#8127) (#8157)
Double-clicking a sketch row inside a component (an inserted fastener's sketch)
activates that component so the sketch is edited in its placement. Finish and
Cancel only closed the sketch, so the component stayed active and every other
body stayed faded.

Edit Sketch now remembers the component and placement active before it
activated the sketch's own (also when sketch.edit activates the component of a
face-attached sketch), and leaving the sketch makes that one active again, as
Finish already puts the history marker back. A component activated while the
sketch is open stays active.

Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:15 +00:00
semmelrinde 944c658c90 Czech: translate 867 messages the catalog was missing (batch 1) (#8188)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:06 +00:00
semmelrinde 8f8df03358 Navigation: Maya and HFSS mouse presets (#6474, #2742) (#8184)
Maya moves the camera with Alt and any button (left orbits, middle pans,
right zooms), middle drag also pans. HFSS pans with Alt+left drag and zooms
with Shift+Alt+left; its Ctrl+left orbit is the drag from empty space every
preset already has, so a Ctrl+click on geometry still adds to the selection.
The help texts are in the catalog for every locale that has the other
presets' help.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:05 +00:00
github-actions[bot] 92fc868aa9 Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:22:20 +00:00
github-actions[bot] 33281cf3bc Regenerate generated files and fold pending files [skip ci] 2026-10-28 13:13:50 +00:00
semmelrinde d64816437a Dutch: translate 942 messages the catalog was missing (batch 1) (#8172)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:02 +00:00
semmelrinde 8210e94b6d Malay: translate 888 messages the catalog was missing (batch 1) (#8216)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:08 +00:00
semmelrinde f6d969f98a Hungarian: translate 946 messages the catalog was missing (batch 1) (#8199)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:06 +00:00
dudedagger42 59faca3850 Canvas and Decal: send the picked face, so a large design takes the image (#5592) (#5593)
* claim: #5592

* Canvas and Decal: send the picked face, so a large design takes the image (#5592)

A click with Canvas or Decal sent only the point, so the engine searched every face of every
body for it and gave up on a large design ("face capture work limit exceeded"). Send the
picked body, face index and path as Create Sketch does; canvas.decal takes that face pick too.

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

* Canvas and Decal test: drop the check that a point alone gives up

On upstream main a sketch on a face by its point alone no longer gives up on this design, so
the test keeps only what the picked face has to do: both images land on it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:11:01 +00:00
semmelrinde 29a2f33664 Galician: translate 888 messages the catalog was missing (batch 1) (#8214)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:10:58 +00:00
sascha2101 9d4d7932ee Sketch: offer an arc length dimension for a selected arc (#6039) (#6264)
* claim: #6039

* Sketch: offer an arc length dimension for a selected arc (#6039)

The arc length dimension already existed in the engine (sketch.dimension with
type arc_length), but nothing in the UI led to it: the Dimension tool and the
context menu always gave an arc its radius.

With exactly one arc selected, the sketch context menu now offers Arc Length
Dimension next to Sketch Dimension, which adds the dimension through the
existing command.

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

* test: move the arc length menu test up to avoid a 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-28 12:10:45 +00:00
github-actions[bot] 54a207cb8b Regenerate generated files and fold pending files [skip ci] 2026-10-28 12:44:09 +00:00
github-actions[bot] 5bf5de49a5 Regenerate generated files and fold pending files [skip ci] 2026-10-28 12:35:35 +00:00
semmelrinde cee995e776 Norwegian Bokmål: translate 693 messages the catalog was missing (batch 1) (#8218)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:10:34 +00:00
Nathan Dane b86c75544e Appearance: read the look chain and replace a look everywhere in one undo step (#6428) 2026-10-28 12:10:31 +00:00
yhamil04 249318cffb Right-click opens the menu despite tiny pointer movement (#5517) (#7939)
* claim: #5517

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

* Right-click opens the menu despite tiny pointer movement (#5517)

A Magic Mouse moves the pointer a fraction of a point as it is clicked.
The right button orbited on any movement, and the menu needed exactly
zero movement in the release frame, so most right-clicks only nudged
the view. The right button now navigates once the press can no longer
be a click (egui's click distance and time), catching up with the
travel since the press, and a right-click is egui's click.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:08:27 +00:00
Colin Faulkingham 6f8ee10a86 Appearance: apply browser actions to all selected bodies (#7935) (#7941)
* claim: #7935

* fix: apply browser appearance changes to selected bodies (#7935)
2026-10-28 12:08:21 +00:00
N3MI-DG 30cb1238f7 Browser: reveal a placed body's row when it is picked or found (#7947) (#7948)
* claim: #7947

* Browser: reveal a placed body's row when it is picked or found (#7947)

The off-screen skip in `entry_row` keeps the reveal target's row alive so it
still has a rect to scroll to, but it matched `app.tree.reveal` (a world body
name) against `Entry::key` (the placement-local name). For a placed body those
never match, so the row returned early and the `scroll_to_rect` below it, which
does use `row_key`, was never reached. `app.tree.reveal` is then cleared at the
end of the frame, so there was no second chance.

Match the skip on `row_key` minus its `b:` prefix, as the `scroll_to_rect`
already does.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-28 12:08:01 +00:00
raykholo db999b2a98 Control protocol: the ui.prefs settings as a sorted list, one per line (#7747) (#7988) 2026-10-28 12:07:43 +00:00
semmelrinde e59decda95 Primitive dialogs: label the size fields in words, not with the parameter names (#7995)
The Box, Cylinder, Cone, Sphere, Torus and Coil dialogs labelled their
size rows with the command's parameter names (length, top_diameter,
section_size), in every language. Each row now takes a message: Length,
Top diameter, Major radius, Section size, translated as the rest of the
dialog is.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:07:37 +00:00
github-actions[bot] aca80f2054 Regenerate generated files and fold pending files [skip ci] 2026-10-28 10:25:10 +00:00
Nathan Dane 0b2efd60b9 Render browser: scene tree with Environment, Lights, Shots and Objects rows, resolved appearances and a Browser selection (#6427) 2026-10-28 09:58:57 +00:00
amolinari-autotex 54f613e4dd Preserve linked component materials and appearances (#7609) (#7616)
* Fix linked component materials and appearances (#7609)

* Linked components: the cached link read carries the source properties

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

* Update Links: a link without recorded inherited properties is read again

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

* Preserve authored linked-face overrides and apply cached source properties

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:33:49 +00:00
github-actions[bot] ae07e36a7d Regenerate generated files and fold pending files [skip ci] 2026-10-28 07:57:59 +00:00
Nathan Dane 18fe82ab26 Render workspace: focus view, compact docks, a Browser chip at narrow widths and a localisation pass (#7376) 2026-10-28 07:29:53 +00:00
github-actions[bot] 372badfc1b Regenerate generated files and fold pending files [skip ci] 2026-10-28 06:43:04 +00:00
Nathan Dane 1781b4d323 Render workspace: move the Inspector or Browser dock to either side with view.render_dock and a header menu (#7230) 2026-10-28 06:15:13 +00:00
github-actions[bot] cf5dc813e2 Regenerate generated files and fold pending files [skip ci] 2026-10-28 04:43:28 +00:00
Nathan Dane b0c8c466b0 Navigation bar: icons for visual styles, selection modes and display (#6585) (#6898)
* Navigation bar: slot model and one-row renderer (#6585)

* Navigation bar: customisation section in Preferences (#6585)

* Navigation bar: icons for visual styles, selection modes and display (#6585)

* Navigation bar: redraw the Bodies, Parts and Components selection icons (#6585)

* Navigation bar: refine the Ground shadow, Ambient occlusion, Parts and Components icons (#6585)

* Navigation bar: solid cube and a longer arc for the Ground shadow icon (#6585)

* Navigation bar: the owner accepts the new icons as final (#6585)
2026-10-28 04:16:44 +00:00
github-actions[bot] 85aaed2ccb Regenerate generated files and fold pending files [skip ci] 2026-10-28 03:48:50 +00:00
Nathan Dane 110efa3598 Render: allow picking in the product view with a Body, Face or Component filter and outline the selection in the product pass only (#6426) 2026-10-28 03:20:43 +00:00
P'tite Tête 9fb41f8378 feat: Start a new selection's preview without waiting for the obsolete one (#7167) (#7261)
- Leave the running preview behind when the selection moves to another setup, asked to stop
- Keep at most two obsolete previews running; streaming, tools and other inputs wait as before
- Keep no result of a cancelled watched evaluation in the shared evaluation cache
- Test the detaching, the cancel reaching the spawned job, and the cache guard

Co-authored-by: P'tite Tête <asso@ptitetete.com>
2026-10-28 01:32:17 +00:00
525 changed files with 134751 additions and 3279 deletions
+21
View File
@@ -429,6 +429,20 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/render_environment.svg | SolidCraft contributors | original Studio icon master (64 grid): dome with a grey cube inside | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_shaded.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_wireframe.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_hidden_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/style_translucent.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_faces_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_bodies.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_features.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_parts.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/select_sketch.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/display.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/grid.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ground_shadow.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/flat/ambient_occlusion.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -581,6 +595,13 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/styles/ui/themes/cga.json | SolidCraft contributors | original SolidCraft theme file using the 16 colours of the IBM Color Graphics Adapter (black, cyan, magenta, and white of its high-intensity palette 1 for the interface) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/1-bit-dark.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-customisation.json` | SolidCraft contributors | Original navigation bar editor translations | MIT OR Apache-2.0 |
| `assets/locales/pending/navbar-slots.json` | SolidCraft contributors | Original navigation slot translations | MIT OR Apache-2.0 |
| assets/locales/bs.json | Tarik Sijercic (tsijercic1) | Original Bosnian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/hr.json | Tarik Sijercic (tsijercic1) | Original Croatian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/sr.json | Tarik Sijercic (tsijercic1) | Original Serbian (ekavian, Cyrillic) translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/finish_edit.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| `assets/locales/pending/sketch-tracking-guides.json` | SolidCraft contributors | Original alignment-guide preference labels and help text | MIT OR Apache-2.0 |
## Code adapted from sibling projects
+6 -1
View File
@@ -268,7 +268,9 @@ a conflict. `crates/doc/src/format/pending/README.md` has an example.
**At release time** the maintainer runs `scripts/fold-format-pending.sh`, which moves the pending
files into `crates/doc/src/format/v<N>/` and fixes that number for good (see
[docs/releasing.md](docs/releasing.md)). Never add a file to a `v<N>/` folder afterwards.
[docs/releasing.md](docs/releasing.md)). Never add a file to a `v<N>/` folder afterwards. Drawings
follow the same rules with their own numbers, from 1, in `crates/doc/src/drawing/format/` (see its
`pending/README.md`), and the same script folds them.
## Lists many changes add to
@@ -287,6 +289,9 @@ and a fresh CI run:
- **Toolbar commands without help** (`BACKLOG` in `crates/ui-egui/src/command_help.rs`): sorted,
one id per line, and it only shrinks. Writing a command's help removes its line; a new command
is never added to it.
- **`ui.prefs` settings** (`docs/control-protocol.md`): one line per setting, sorted by name, in
the list under the protocol table; add a new setting on its own line rather than editing a shared
row.
- **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`
Generated
+1
View File
@@ -4238,6 +4238,7 @@ dependencies = [
"ureq",
"windows-sys 0.61.2",
"winit",
"x11rb",
]
[[package]]
+1 -1
View File
@@ -653,7 +653,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
if let Some(s) = &scope {
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
eprintln!(
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
"solidcraft mcp: files limited to {} and the folders of designs opened or saved here (--allow-path DIR adds one, --allow-any-path lifts the limit)",
roots.join(", ")
);
let design_dir = flag(args, "--in")
+6 -2
View File
@@ -40,6 +40,8 @@ pub enum Intent {
Open,
/// Add its bodies to the open design (STEP, 3MF, STL).
Insert,
/// Import it as a theme: it was dropped on Preferences' Import tile (#8226).
Theme,
}
/// A stored design, as the file panel lists it.
@@ -615,13 +617,15 @@ fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
}
/// Read files dropped on the page.
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
/// `theme`: the pointer is over Preferences' Import tile, so the files are themes.
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput, theme: bool) {
let intent = if theme { Intent::Theme } else { Intent::Insert };
for f in std::mem::take(&mut raw.dropped_files) {
let files = files.clone();
spawn_local(async move {
let name = f.path().file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "dropped".into());
match f.bytes_async().await {
Ok(b) => files.arrive(name, b, Intent::Insert),
Ok(b) => files.arrive(name, b, intent),
Err(e) => files.say(format!("{name}: {e}"), true),
}
});
+59 -52
View File
@@ -271,6 +271,10 @@ mod web {
!companion
});
for (name, bytes, intent) in incoming {
if intent == Intent::Theme {
solidcraft_ui_egui::prefs::import_theme_bytes(&mut self.app, bytes);
continue;
}
let design = solidcraft_engine::io::is_design_path(&name);
let path = if design && intent == Intent::Open {
// A design from the computer is kept in the browser (a copy), so Save works.
@@ -287,7 +291,7 @@ mod web {
};
match intent {
Intent::Open => self.app.open_path(&path),
Intent::Insert => self.app.drop_path(&path),
Intent::Insert | Intent::Theme => self.app.drop_path(&path),
}
self.files.panel.set(false);
}
@@ -353,7 +357,8 @@ mod web {
}
let mut act: Option<(&'static str, String)> = None;
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
.cards(3.0)
// Room for long names; the kit still caps the window at the browser's width.
.cards(4.0)
.show_body(ctx, |ui| {
ui.horizontal(|ui| {
if ui_kit::button(ui, "Open from computer…", ui_kit::Button::Normal).clicked() {
@@ -381,61 +386,62 @@ mod web {
return;
}
let entries = self.files.entries.borrow().clone();
if entries.is_empty() {
ui.label("Designs you save appear here. They stay in this browser.");
}
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
egui::Grid::new("opfs_files").num_columns(3).spacing([12.0, 6.0]).show(ui, |ui| {
// The name takes the width the size and the actions leave (#8097).
let empty = "Designs you save appear here. They stay in this browser.";
ui_kit::name_list(ui, "opfs_files", &["Name", "Size", "Actions"], empty, |list| {
for e in &entries {
match &mut self.renaming {
Some((from, to)) if *from == e.name => {
let r = ui_kit::cell_editor(ui, ui.make_persistent_id(("stored_file_name", from.as_str())), to, "", false);
if std::mem::take(&mut self.rename_focus) {
r.request_focus();
}
if r.lost_focus() {
act = Some(if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
("rename", to.clone())
} else {
("rename_cancel", String::new())
});
list.row(&e.name, |row| {
match &mut self.renaming {
Some((from, to)) if *from == e.name => row.cell(|ui| {
let r =
ui_kit::cell_editor(ui, ui.make_persistent_id(("stored_file_name", from.as_str())), to, "", false);
if std::mem::take(&mut self.rename_focus) {
r.request_focus();
}
if r.lost_focus() {
act = Some(if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
("rename", to.clone())
} else {
("rename_cancel", String::new())
});
}
}),
_ => {
row.name(&e.name);
}
}
_ => {
ui.label(&e.name);
}
}
ui.label(format!("{:.1} KB", e.size as f64 / 1024.0));
ui.horizontal(|ui| {
if ui_kit::button(ui, "Open", ui_kit::Button::Small).clicked() {
act = Some(("open", e.name.clone()));
}
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
row.cell(|ui| ui.label(format!("{:.1} KB", e.size as f64 / 1024.0)));
row.cell(|ui| {
if ui_kit::button(ui, "Open", ui_kit::Button::Small).clicked() {
act = Some(("open", e.name.clone()));
}
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
.clicked()
{
act = Some(("insert", e.name.clone()));
}
if ui_kit::button(ui, "Rename", ui_kit::Button::Small).clicked() {
self.renaming = Some((e.name.clone(), e.name.clone()));
self.rename_focus = true;
}
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
.on_hover_text("A copy in a file you choose; the stored design stays here")
.clicked()
{
act = Some(("insert", e.name.clone()));
}
if ui_kit::button(ui, "Rename", ui_kit::Button::Small).clicked() {
self.renaming = Some((e.name.clone(), e.name.clone()));
self.rename_focus = true;
}
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
.on_hover_text("A copy in a file you choose; the stored design stays here")
.clicked()
{
act = Some(("save_copy", e.name.clone()));
}
if ui_kit::button(ui, "Download", ui_kit::Button::Small).clicked() {
act = Some(("download", e.name.clone()));
}
if ui_kit::button(ui, "Delete", ui_kit::Button::Small).clicked() {
act = Some(("delete", e.name.clone()));
}
{
act = Some(("save_copy", e.name.clone()));
}
if ui_kit::button(ui, "Download", ui_kit::Button::Small).clicked() {
act = Some(("download", e.name.clone()));
}
if ui_kit::button(ui, "Delete", ui_kit::Button::Small).clicked() {
act = Some(("delete", e.name.clone()));
}
});
});
ui.end_row();
}
});
});
@@ -630,7 +636,8 @@ mod web {
ctx.request_repaint_after(delay);
}
if self.launch.persists() {
files::take_drops(&self.files, raw);
let theme = solidcraft_ui_egui::prefs::theme_drop_target(&self.app, ctx);
files::take_drops(&self.files, raw, theme);
} else {
raw.dropped_files.clear();
}
+5
View File
@@ -96,6 +96,11 @@ libc = "0.2"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
# Bringing the window forward on X11 when a close asks about unsaved designs (src/come_forward.rs):
# the window's id, and the window manager request winit doesn't send (MIT OR Apache-2.0; both
# already in the tree through eframe and winit).
raw-window-handle = "0.6"
x11rb = { version = "0.13", default-features = false }
[build-dependencies]
embed-resource = "3.0.11"
+234
View File
@@ -0,0 +1,234 @@
//! Bringing the window forward when a close stops at "save changes?".
//!
//! A close from the taskbar or a window switcher reaches the window while another app has the
//! focus. The prompt it opens is only seen once the window comes forward, so the window asks for
//! the focus then. A window the user is already in is left alone.
//!
//! winit's Focus asks as an application, which a window manager with focus-stealing prevention
//! (KWin's default) refuses for a window used before the switch away: it only marks the taskbar
//! entry. On X11 the request is sent again with the source EWMH gives "direct user actions"
//! (`_NET_ACTIVE_WINDOW`, source 2), which is what this is: the prompt is the rest of the user's
//! close. Native Wayland has no way for a window to raise itself, so there the attention request
//! (xdg-activation) lets the compositor raise it or mark its taskbar entry.
//!
//! When: not at once. KWin raises the window, then the taskbar's closing context menu hands the
//! focus back to the app that had it, a few milliseconds later, and the window would flicker
//! forward and back. So the window asks once the menu is gone, `FIRST_AFTER` the close. It asks
//! again only if the focus is taken back right after it came (a slow menu) or never came; a later
//! switch is the user's own, and the window stays where the user put it.
use std::time::{Duration, Instant};
/// From the close to the first request: past the taskbar menu's focus hand-back (2 to 11 ms on
/// KDE Plasma 6), too short to notice.
const FIRST_AFTER: Duration = Duration::from_millis(120);
/// After the first request, how long the window asks again when it loses the focus: short of
/// anything the user does next.
const KEEP_ASKING: Duration = Duration::from_millis(400);
/// The least time between two requests.
const AGAIN_AFTER: Duration = Duration::from_millis(100);
/// Focus lost this soon after a request was taken back by the taskbar menu; later, by the user.
const TAKEN_BACK_WITHIN: Duration = Duration::from_millis(50);
/// The window coming forward after a close that asks about unsaved designs.
#[derive(Default)]
pub struct ComeForward {
/// The first request and the end of asking; `None`: not asking.
window: Option<(Instant, Instant)>,
/// The last request.
last: Option<Instant>,
/// The window has had the focus since the last request.
held: bool,
}
/// What a frame does while the window is coming forward.
#[derive(Debug, PartialEq)]
enum Step {
/// Ask for the focus now.
Ask,
/// Look again after this long.
Wait(Duration),
/// Done asking.
Done,
}
impl ComeForward {
/// A close stopped at the prompt: ask for the focus shortly.
pub fn start(&mut self, now: Instant) {
let first = now + FIRST_AFTER;
self.window = Some((first, first + KEEP_ASKING));
self.last = None;
self.held = false;
}
/// Every frame: ask once the taskbar menu is gone, and again while the focus is taken back.
pub fn tick(&mut self, ctx: &egui::Context, frame: &eframe::Frame) {
if self.window.is_none() {
return;
}
let (focused, minimized) = ctx.input(|i| (i.viewport().focused, i.viewport().minimized));
// The app may be idle: a frame to look again.
match self.step(Instant::now(), focused) {
Step::Ask => {
bring(ctx, frame, minimized);
ctx.request_repaint_after(AGAIN_AFTER);
}
Step::Wait(after) => ctx.request_repaint_after(after),
Step::Done => {}
}
}
fn step(&mut self, now: Instant, focused: Option<bool>) -> Step {
let Some((first, until)) = self.window else { return Step::Done };
if now >= until {
self.window = None;
return Step::Done;
}
if now < first {
return Step::Wait(first - now);
}
let Some(last) = self.last else {
if focused == Some(true) {
// The user went back to the window already.
self.window = None;
return Step::Done;
}
self.last = Some(now);
return Step::Ask;
};
if focused == Some(true) {
self.held = true;
return Step::Wait(until - now);
}
if self.held {
if now.duration_since(last) >= TAKEN_BACK_WITHIN {
// The user switched away: leave the window there.
self.window = None;
return Step::Done;
}
self.held = false;
}
let next = last + AGAIN_AFTER;
if now < next {
return Step::Wait(next - now);
}
self.last = Some(now);
Step::Ask
}
}
/// Ask for the focus once.
fn bring(ctx: &egui::Context, frame: &eframe::Frame, minimized: Option<bool>) {
#[cfg(all(unix, not(target_os = "macos")))]
let raised = match x11_activate(frame) {
Ok(sent) => sent,
Err(e) => {
log::warn!("bringing the window forward through X11: {e}");
false
}
};
#[cfg(not(all(unix, not(target_os = "macos"))))]
let raised = {
let _ = frame;
false
};
for c in commands(minimized, raised) {
ctx.send_viewport_cmd(c);
}
}
/// The viewport commands for a window in the background; `raised`: the X11 request went out,
/// which also restores a minimized window.
fn commands(minimized: Option<bool>, raised: bool) -> Vec<egui::ViewportCommand> {
if raised {
return vec![egui::ViewportCommand::Focus];
}
let mut commands = Vec::new();
if minimized == Some(true) {
commands.push(egui::ViewportCommand::Minimized(false));
}
commands.push(egui::ViewportCommand::Focus);
commands.push(egui::ViewportCommand::RequestUserAttention(egui::UserAttentionType::Informational));
commands
}
/// Ask the X11 window manager to activate the window as a user action. `Ok(false)`: the window
/// isn't an X11 window (native Wayland).
#[cfg(all(unix, not(target_os = "macos")))]
fn x11_activate(frame: &eframe::Frame) -> Result<bool, String> {
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{ClientMessageEvent, ConnectionExt, EventMask};
let handle = frame.window_handle().map_err(|e| e.to_string())?;
let window = match handle.as_raw() {
RawWindowHandle::Xlib(h) => u32::try_from(h.window).map_err(|e| e.to_string())?,
RawWindowHandle::Xcb(h) => h.window.get(),
_ => return Ok(false),
};
// A connection of its own: winit's isn't reachable, and a close that asks is rare.
let (conn, screen) = x11rb::connect(None).map_err(|e| e.to_string())?;
let root = conn.setup().roots.get(screen).ok_or("no X11 screen")?.root;
let atom = conn.intern_atom(false, b"_NET_ACTIVE_WINDOW").map_err(|e| e.to_string())?.reply().map_err(|e| e.to_string())?.atom;
// Source 2: a direct user action; the time is the server's current time.
let event = ClientMessageEvent::new(32, window, atom, [2, x11rb::CURRENT_TIME, 0, 0, 0]);
conn.send_event(false, root, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, event).map_err(|e| e.to_string())?;
conn.flush().map_err(|e| e.to_string())?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::{AGAIN_AFTER, ComeForward, FIRST_AFTER, KEEP_ASKING, Step};
use eframe::egui::{UserAttentionType, ViewportCommand as C};
use std::time::{Duration, Instant};
/// The first request waits until the taskbar menu has handed the focus back. The window asks
/// again when the focus is taken back right after it came, or never came, no more often than
/// every `AGAIN_AFTER` and only for `KEEP_ASKING`; a later switch away is the user's.
#[test]
fn the_window_asks_once_the_taskbar_menu_is_gone() {
let t0 = Instant::now();
let ms = |n: u64| Duration::from_millis(n);
let at = |n: u64| t0 + ms(n);
let (first, again, keep) = (FIRST_AFTER.as_millis() as u64, AGAIN_AFTER.as_millis() as u64, KEEP_ASKING.as_millis() as u64);
let mut f = ComeForward::default();
assert_eq!(f.step(t0, Some(false)), Step::Done, "not started");
f.start(t0);
assert_eq!(f.step(t0, Some(false)), Step::Wait(FIRST_AFTER), "the menu is still closing");
assert_eq!(f.step(at(5), Some(false)), Step::Wait(ms(first - 5)));
assert_eq!(f.step(at(first), Some(false)), Step::Ask);
assert_eq!(f.step(at(first + 3), Some(true)), Step::Wait(ms(keep - 3)), "raised");
// A slow menu takes the focus back 10 ms later: ask again, not before AGAIN_AFTER.
assert_eq!(f.step(at(first + 10), Some(false)), Step::Wait(ms(again - 10)));
assert_eq!(f.step(at(first + again), Some(false)), Step::Ask);
assert_eq!(f.step(at(first + again + 3), Some(true)), Step::Wait(ms(keep - again - 3)));
// The user clicks another app 200 ms later: the window stays behind.
assert_eq!(f.step(at(first + again + 200), Some(false)), Step::Done);
assert_eq!(f.step(at(first + again + 300), Some(false)), Step::Done);
// Never raised (the request was refused): ask again every AGAIN_AFTER, for KEEP_ASKING.
f.start(t0);
assert_eq!(f.step(at(first), None), Step::Ask, "unknown focus counts as out of focus");
assert_eq!(f.step(at(first + again), Some(false)), Step::Ask);
assert_eq!(f.step(at(first + keep), Some(false)), Step::Done);
// Back in the window before the first request: nothing to do.
f.start(t0);
assert_eq!(f.step(at(first), Some(true)), Step::Done);
}
/// Without the X11 request: focus, and the attention mark where focus is refused; a minimized
/// window is restored first.
#[test]
fn a_window_closed_from_the_background_comes_forward() {
let forward = [C::Focus, C::RequestUserAttention(UserAttentionType::Informational)];
assert_eq!(super::commands(Some(false), false), forward);
assert_eq!(super::commands(None, false), forward, "unknown counts as not minimized");
let minimized = super::commands(Some(true), false);
assert_eq!(minimized.first(), Some(&C::Minimized(false)));
assert_eq!(minimized.get(1..), Some(&forward[..]));
// The window manager took the X11 request: nothing left to mark.
assert_eq!(super::commands(Some(true), true), [C::Focus]);
}
}
+10
View File
@@ -20,6 +20,7 @@
#[cfg(not(target_os = "macos"))]
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
mod come_forward;
mod control_server;
mod device_lost;
#[cfg(target_os = "windows")]
@@ -84,6 +85,8 @@ struct App {
frames: u32,
/// Started over the control channel: the window doesn't ask for the foreground.
take_focus_unwanted: bool,
/// Coming forward after a close that asks about unsaved designs.
come_forward: come_forward::ComeForward,
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
gpu: Option<LostWatch>,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
@@ -139,7 +142,13 @@ impl eframe::App for App {
if ctx.input(|i| i.viewport().close_requested()) && !self.solve.quit_requested {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
solidcraft_ui_egui::documents::request_quit(&mut self.solve);
// Closed from the taskbar or a window switcher while another app had the focus: the
// "save changes?" prompt is only seen once the window comes forward.
if !self.solve.quit_requested {
self.come_forward.start(std::time::Instant::now());
}
}
self.come_forward.tick(ctx, _frame);
#[cfg(target_os = "macos")]
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
self.solve.open_path_async(&f);
@@ -1060,6 +1069,7 @@ fn main() -> eframe::Result {
starting,
frames: 0,
take_focus_unwanted: control_port.is_some(),
come_forward: come_forward::ComeForward::default(),
gpu,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
space_mouse,
+3
View File
@@ -27,6 +27,9 @@ error = { master = "status_error", accent = "surface" }
warning = { master = "status_warning", accent = "mesh" }
ground = "c_fix"
# Visual style: Shaded with edges is the generic style icon.
style_shaded_edges = "style"
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
sketch_break = { master = "break", accent = "sketch" }
sketch_cancel = { master = "close", accent = "sketch" }
@@ -1,6 +1,8 @@
accessibility false
activate false
agent_chat false
align false
ambient_occlusion false
appearance false
arc3 false
arc_center false
@@ -83,6 +85,8 @@ decal false
delete false
delete_face false
dimension false
display false
dots false
draft false
draft_analysis false
drive false
@@ -101,6 +105,7 @@ eye false
fastener false
fillet false
finish false
finish_edit false
fit false
fit_section false
flange false
@@ -110,7 +115,9 @@ folder false
frame_member false
gear false
graph false
grid false
ground false
ground_shadow false
help false
hem false
hole false
@@ -210,6 +217,11 @@ section false
select false
send false
settings false
settings_keyboard false
settings_monitor false
settings_mouse false
settings_palette false
settings_plug false
sheet_rules false
shell false
shield false
@@ -247,6 +259,11 @@ star false
status_error false
status_recovery false
style false
style_hidden_edges false
style_shaded false
style_shaded_edges false
style_translucent false
style_wireframe false
suppress false
surface false
surface_blend false
+15
View File
@@ -6,6 +6,7 @@
"agent_chat": "4a9ae0e7e2c360b4858b3175674c93d096705c92318fecd481e849f3ae650bf4",
"agent_link": "2f20d89a0e3e9b28744910dddf7f282a1099ccd91aa160c9841c5b1d9ea9ed0f",
"align": "05615db7dd9e4be56ae898e59a68c548b8a8447794442a9a394940651204bc4f",
"ambient_occlusion": "973f8ef2326aeaf0face576e5a2c79c7ef3d13638b2c51a5aad199e4ef7edb02",
"appearance": "b3162edd92291a5ef402e8499cd52be05beb2fa7eda641163c3f6e98325de2bf",
"arc3": "fdca3d501fd557ac989376a12e9c4fa8231fde2b455c20724d1079a466153e9c",
"arc_center": "3a5fdeb1d64fc6741f944c4435870b05fccab62293e270da692c6681155cbc51",
@@ -88,6 +89,7 @@
"dimension": "a926b7c64bc8ca99d8d6dcbb81b95235bd3fc34fbed049aca8b01b4d1147c15d",
"disclosure_closed": "49a350c6f2efae78de0be96c524ebee1fd1291ea449a84224b88d37164a4e807",
"disclosure_open": "d13ce3f97e3e362a9904fa2bf3412f3f00e4136bc23538b92d41483961d280de",
"display": "58b45e5e2230eaf2c80f74b3be2e02a23714c90d6fb03b59ed093a1949993163",
"dots": "600f7ff209d93145fc938bc4f6a1e34473e6b9ee22a5b4718f133bc7da5ee58d",
"draft": "9c0fc361cacd0ad95ccd54794f4825dfc9901a83a3de752a588faec365d0ee09",
"draft_analysis": "eb39c0f90e9cf226da6307365a76015e900dd692dc3f0fdc8f02fe717226261f",
@@ -117,7 +119,9 @@
"folder": "c19f5230df45a1bda1e92e89de38bba21c91e6eca0517eb15847a7712012d1e5",
"gear": "25a1ca48a587850da1104a46005a330873ed196246e3d04e12cf61f26ad2b0d5",
"graph": "7156caedd2dbfa2ae53da18b7a2204b8db66c45126859d36caf049a3fd4335a4",
"grid": "084b5040d57e9d5183b3db1fad0d17a13ab5bae64676eef703cc06622f9c7438",
"ground": "23e2471a9d6872e83028e0e219e330cef1930a4d89456a0fefa1bda8a7e3eb81",
"ground_shadow": "a96809ed87f5047c2d7f1274908ed7b65b180613322ac9c10eb0f9d6296c1b66",
"help": "8ce4060577483dbdb510be147d2827de50ac20029354008fca55abc0c7edf05d",
"hem": "f9b1aafbedcf3dc90c26be308814638e0095fbe31b0647092507343fa60af011",
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
@@ -263,6 +267,12 @@
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
"select_bodies": "8e6e61e083e6097535da3b98d41b607b40fc81b72bbd8c7fb903618c5b35c568",
"select_components": "3e6cc847b334e92ce7ec5e5f35dfa929572e0974239790d76563ca4e20b26789",
"select_faces_edges": "7b99b5370b1647bf8cea57df7a1053caab641c2f4a94c4d797b9679e6f38d6f1",
"select_features": "b050bc19ce86d135cbe50998be681555110e92e179c4c7b101170c6b62c19943",
"select_parts": "569adc5595a64187411d8ffeea69eab3b76ddfa33aeead6fd728efdd993bcccc",
"select_sketch": "b0605b34a14d4173f7ed80d877109ec0b891a1b395bcab5743a603ed2eae240a",
"send": "f83b6cdab792f9567a81d46d7828ed20795513c57a2c7d00fbbe6bd14d2f5848",
"settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a",
"settings_keyboard": "4968c70788094365af9c6e2b1c15b2aac45cf21212a14b8a256daf1ffe17f41a",
@@ -309,6 +319,11 @@
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_hidden_edges": "a707dfc0bdcabe9144865f569abfcb478e493143cbd4331efc1404cf3082f6d2",
"style_shaded": "6e5f8f9a9030ab3717f8b2192bbfd3f6b515ad398acf6cb41bc24f1298bc73b4",
"style_shaded_edges": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
"style_translucent": "c46b0cd485cfb9023514908357048b5cba9017a0c4976239da1e98f50e4ab5f9",
"style_wireframe": "af7e575d378b5b6850bee9d3ca384efab95c8d4c876c5e3524f3ea771c4380a3",
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
"surface_blend": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc",
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ambient occlusion</title>
<g opacity="0.22"><path d="M13 51L51 51A38 38 0 0 0 13 13Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.38"><path d="M13 51L40 51A27 27 0 0 0 13 24Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.62"><path d="M13 51L29 51A16 16 0 0 0 13 35Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M13 10L13 51L54 51" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 758 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Display</title>
<path d="M10 22L54 22M10 42L54 42" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="24" cy="22" r="10.5" fill="#ffffff"/>
<circle cx="42" cy="42" r="10.5" fill="#ffffff"/>
<circle cx="24" cy="22" r="6.4" fill="currentColor"/>
<circle cx="42" cy="42" r="6.4" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Grid</title>
<path d="M12 12L52 12L52 52L12 52Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25.3 12L25.3 52M38.7 12L38.7 52M12 25.3L52 25.3M12 38.7L52 38.7" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Ground shadow</title>
<path d="M32 4.8 L48.1 13.9 L48.1 32.1 L32 41.2 L15.9 32.1 L15.9 13.9Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.34 31.35A25 11 0 1 0 54.66 31.35" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Bodies</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Components</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25ZM41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M37.46 18.35L22.91 26.75L22.91 43.55" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Faces and edges</title>
<path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L9 19M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Features</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 19L32 32M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M42 19C42 22.2 37.52 24.8 32 24.8C26.48 24.8 22 22.2 22 19C22 15.8 26.48 13.2 32 13.2C37.52 13.2 42 15.8 42 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 632 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Parts</title>
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25Z" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 585 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Sketch features</title>
<path d="M10 46C18 14 46 14 54 46" fill="none" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="10" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="54" cy="46" r="8.6" fill="#ffffff"/>
<circle cx="32" cy="22.5" r="8.6" fill="#ffffff"/>
<circle cx="10" cy="46" r="5.8" fill="currentColor"/>
<circle cx="54" cy="46" r="5.8" fill="currentColor"/>
<circle cx="32" cy="22.5" r="5.8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded with hidden edges</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L15.44 22.64M32 32L48.56 22.64M32 32L32 50.72" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 13.28M32 32L48.56 41.36M32 32L15.44 41.36" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="4.5 4"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Shaded</title>
<g opacity="0.5"><path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<g opacity="0.8"><path d="M9 19 L32 32 L32 58 L9 45Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 32 L55 19 L55 45 L32 58Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Translucent</title>
<defs>
<clipPath id="tr-clip"><path d="M32 8.6 L52.7 20.3 L52.7 43.7 L32 55.4 L11.3 43.7 L11.3 20.3Z"/></clipPath>
</defs>
<g clip-path="url(#tr-clip)"><path d="M-71 72L9 -8M-60.5 72L19.5 -8M-50 72L30 -8M-39.5 72L40.5 -8M-29 72L51 -8M-18.5 72L61.5 -8M-8 72L72 -8M2.5 72L82.5 -8M13 72L93 -8M23.5 72L103.5 -8M34 72L114 -8M44.5 72L124.5 -8M55 72L135 -8" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/></g>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.76 20.56M32 32L52.24 20.56M32 32L32 54.88" fill="none" stroke="#ffffff" stroke-width="6.4" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L11.3 20.3M32 32L52.7 20.3M32 32L32 55.4" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Wireframe</title>
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 32L32 6M32 32L55 19M32 32L55 45M32 32L32 58M32 32L9 45M32 32L9 19" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Finish Edit</title>
<path d="M50 32L14 32M25 21L14 32L25 43" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+878 -1
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -33,6 +33,7 @@
"All open designs": "Alle åbne designs",
"Allowed expressions, separated by semicolons": "Tilladte udtryk adskilt af semikolon",
"Also deletes": "Sletter også",
"Also place them from the origin (fully constrained)": "Placer dem også fra origo (fuldt fastlåst)",
"Anchor": "Forankring",
"Angle 2": "Vinkel 2",
"Animate": "Animér",
@@ -158,6 +159,7 @@
"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",
"Dimension new shapes": "Mål nye figurer",
"Direction": "Retning",
"Discard": "Kassér",
"Distance": "Afstand",
@@ -169,6 +171,8 @@
"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",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Hver ny figur får også mål fra skitsens origo, indtil intet af den er frit. En figur, der allerede er knyttet til origo eller til placeret geometri, får ingen.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Hver figur, du tegner, får alle sine mål i de størrelser, du tegnede (indtastede værdier vinder). En størrelse, som skitsen allerede fastlægger, udelades.",
"Edges": "Kanter",
"Edit": "Redigér",
"Edit Profile Sketch": "Redigér profilskitse",
@@ -1190,7 +1194,9 @@
"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.hfss.help": "Alt+træk med venstre museknap panorerer, Shift+Alt+venstre zoomer, Ctrl+træk med venstre fra et tomt område 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.maya.help": "Alt+træk med venstre museknap roterer visningen, Alt+midterste panorerer, Alt+højre zoomer; træk med midterste museknap panorerer også",
"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",
@@ -1209,6 +1215,11 @@
"no picture": "intet billede",
"none": "ingen",
"parts.automatic_name": "automatisk",
"parts.dimension.d": "Nominel diameter",
"parts.dimension.height": "Højde",
"parts.dimension.pitch": "Stigning",
"parts.dimension.thickness": "Tykkelse",
"parts.dimension.width": "Bredde",
"pattern.alignment_2": "Justering 2",
"pattern.alignment_hint": "Antal omfatter originalen. Centrér fordeler kopierne ligeligt; Vend vælger siden med den ekstra kopi ved et lige samlet antal.",
"pattern.direction_1": "Retning 1",
@@ -1388,6 +1399,13 @@
"render.return_design": "Tilbage til Design",
"render.save_setup": "Gem opsætning",
"render.setup_name": "Opsætningsnavn",
"render.shots_camera_changed": "Kameraet er ændret, siden denne optagelse blev gemt",
"render.shots_name_help": "Indtast et unikt navn til den aktuelle optagelse.",
"render.shots_no_result": "Ingen rendering endnu",
"render.shots_result": "Rendering {number} · {width} × {height}",
"render.shots_revert": "Gendan",
"render.shots_save": "Gem optagelse",
"render.shots_update": "Opdater optagelse",
"render.unavailable_preview": "Denne opsætning bruger en type forhåndsvisning, der ikke er tilgængelig.",
"render.unsaved": "Ændringer i forhåndsvisning er ikke gemt",
"render.use_preview": "Brug integreret forhåndsvisning",
@@ -2057,6 +2075,7 @@
"ui.heading_utilities": "VÆRKTØJER",
"ui.heading_utility": "VÆRKTØJ",
"ui.heading_weldments": "SVEJSNING",
"ui.height": "Højde",
"ui.help": "Hjælp",
"ui.hide": "Skjul",
"ui.hideAxes": "Skjul akser",
@@ -2095,6 +2114,7 @@
"ui.language": "Sprog",
"ui.layout_compact": "Kompakt",
"ui.layout_floating": "Flydende",
"ui.length": "Længde",
"ui.length_decimals": "Længdedecimaler",
"ui.light": "Lys",
"ui.line": "Linje",
@@ -2182,6 +2202,7 @@
"ui.pinToShortcuts": "Fastgør til genveje",
"ui.pinToToolbar": "Fastgør til værktøjslinje",
"ui.pipe": "Rør",
"ui.pitch": "Stigning",
"ui.placement": "Placering",
"ui.plane": "Plan",
"ui.plane_at_angle": "Plan i vinkel",
@@ -2365,6 +2386,7 @@
"ui.visual_shaded_with_edges": "Skyggelagt med kanter",
"ui.visual_style": "Visuel stil",
"ui.visual_wireframe": "Trådmodel",
"ui.width": "Bredde",
"ui.workspace": "arbejdsområde",
"ui.workspace_animation_description": "Eksploderede visninger, bevægelsesstudier og storyboards",
"ui.workspace_available": "Tilgængelig",
+624 -2
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -17,6 +17,8 @@
"cmd.sketch_slot_center_point": "Centre Point Slot",
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
"prefs.sketch_tracking_guides": "Centre and midpoint alignment guides",
"prefs.sketch_tracking_guides_note": "Hover over a circle centre or straight-edge midpoint, then move horizontally or vertically to show a temporary dotted guide.",
"prefs.theme_follow": "Follow a theme file",
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
File diff suppressed because it is too large Load Diff
+201
View File
@@ -1,4 +1,5 @@
{
"(active rule)": "(regla activa)",
"0 turns it off": "0 lo desactiva",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Abre el borrador de GitHub de abajo y pega el informe copiado al portapapeles.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Abre el borrador de GitHub de abajo. El título y el informe se rellenarán automáticamente.",
@@ -26,6 +27,7 @@
"All open designs": "Todos los diseños abiertos",
"Allowed expressions, separated by semicolons": "Expresiones permitidas, separadas por punto y coma",
"Also deletes": "También elimina",
"Also place them from the origin (fully constrained)": "Situarlas también desde el origen (totalmente restringidas)",
"Angle 2": "Ángulo 2",
"Animate": "Animar",
"Appearance name": "Nombre de apariencia",
@@ -35,12 +37,16 @@
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Aplique primero la apariencia. Exporta las apariencias asignadas y los materiales creados en este diseño.",
"Arc length": "Longitud del arco",
"Area": "Área",
"As Picked": "Según la selección",
"As-Built": "Tal como se construyó",
"Assign Rule": "Asignar regla",
"Attachment": "Vinculación",
"BLEND SURFACE": "SUPERFICIE DE TRANSICIÓN",
"BOUNDARY SURFACE": "SUPERFICIE DE CONTORNO",
"BROWSER": "EXPLORADOR",
"Ball": "Rótula",
"Base Height": "Altura de la base",
"Bend Line": "Línea de plegado",
"Bend Line Position": "Posición de la línea de plegado",
"Bend Position": "Posición del pliegue",
"Bend Radius": "Radio de plegado",
@@ -48,6 +54,7 @@
"Bends": "Pliegues",
"Bodies": "Cuerpos",
"Body": "Cuerpo",
"Both": "Ambos",
"Boundary": "Contorno",
"Bounding box": "Caja delimitadora",
"CHECK": "COMPROBAR",
@@ -70,6 +77,7 @@
"Checked": "Comprobado",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Elige un plano nuevo. La geometría y las cotas del boceto se mantienen en sus coordenadas bidimensionales.",
"Choose…": "Elegir…",
"Circle or sphere": "Círculo o esfera",
"Circular": "Circular",
"Circumference": "Circunferencia",
"Clockwise": "Horario",
@@ -81,6 +89,7 @@
"Config Table": "Tabla de config.",
"Configuration": "Configuración",
"Configuration rules": "Reglas de configuración",
"Constant radius": "Radio constante",
"Contact": "Contacto",
"Contact Sets": "Conjuntos de contacto",
"Contact sets": "Conjuntos de contacto",
@@ -105,6 +114,8 @@
"Curvature (G2)": "Curvatura (G2)",
"Curvature change (G3)": "Cambio de curvatura (G3)",
"Customize Toolbar": "Personalizar barra de herramientas",
"Cylindrical": "Cilíndrica",
"Deboss": "Grabado",
"Delete": "Eliminar",
"Delete all": "Eliminar todo",
"Delete rule": "Eliminar regla",
@@ -114,6 +125,7 @@
"Dependents": "Dependientes",
"Designs you open or save appear here.": "Aquí aparecen los diseños que abras o guardes.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por «Hasta objeto». Elige «Ángulo» para modificar la extensión angular.",
"Dimension new shapes": "Acotar las formas nuevas",
"Direction": "Dirección",
"Discard": "Descartar",
"Do not export hidden edges": "No exportar aristas ocultas",
@@ -122,6 +134,9 @@
"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": "Doble clic con el botón central en la vista: Inicio (vista inicial que encuadra lo visible), Ajustar (encuadrar sin cambiar la orientación) o nada",
"Drive Joint": "Accionar unión",
"Duration": "Duración",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada forma nueva también se acota desde el origen del croquis hasta que no quede nada libre. Una forma ya unida al origen o a geometría situada no recibe ninguna.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que dibujas recibe todas sus cotas con los tamaños dibujados (los valores escritos tienen prioridad). Se omite un tamaño que el croquis ya fija.",
"Edge": "Arista",
"Edges": "Aristas",
"Edit": "Editar",
"Edit Profile Sketch": "Editar boceto del perfil",
@@ -129,6 +144,7 @@
"Enabled": "Activado",
"End bulge": "Abombamiento final",
"End continuity": "Continuidad final",
"Equal Distance": "Distancia igual",
"Explode": "Explosionar",
"Explode Text": "Descomponer texto",
"Export Animation…": "Exportar animación…",
@@ -148,6 +164,8 @@
"Face attachment lost": "Se ha perdido la vinculación de la cara",
"Faces": "Caras",
"Faces / edges / vertices": "Caras / aristas / vértices",
"Faces/Body": "Caras/Cuerpo",
"Factor must be a positive number": "El factor debe ser un número positivo",
"Fastener": "Sujeción",
"Favorites": "Favoritos",
"Favourite": "Favorito",
@@ -156,20 +174,28 @@
"Filter": "Filtrar",
"Find in Browser": "Buscar en el explorador",
"Finish": "Acabado",
"First edge": "Primera arista",
"First point": "Primer punto",
"Flange Gap": "Separación de pestaña",
"Flat Pattern": "Desarrollo plano",
"Flat size": "Tamaño del desarrollo",
"Follow camera (Look At sketches)": "Seguir cámara (orientar hacia los bocetos)",
"Follow the camera (Look At sketches)": "Seguir la cámara (orientar hacia los bocetos)",
"Font": "Fuente",
"Free Move": "Movimiento libre",
"From Bodies": "Desde cuerpos",
"From Snap": "Desde el enganche",
"From edges": "Aristas de origen",
"Full Length": "Longitud total",
"Gap": "Separación",
"Grid": "Cuadrícula",
"HISTORY": "HISTORIAL",
"Height": "Altura",
"Hem Gap": "Separación de reborde",
"Hidden edges": "Aristas ocultas",
"History layout": "Disposición del historial",
"Hole Depth": "Profundidad del agujero",
"Hole Diameter": "Diámetro del agujero",
"Holes": "Agujeros",
"Holes (edges on them)": "Agujeros (aristas que contienen)",
"Home view": "Vista de inicio",
@@ -185,10 +211,14 @@
"Isocurve": "Isocurva",
"Issue title": "Título de la incidencia",
"JOINTS": "UNIONES",
"Joint 1": "Unión 1",
"Joint 2": "Unión 2",
"Joint Origin1": "Origen de unión1",
"K Factor": "Factor K",
"Keep (points on it)": "Conservar (puntos sobre ella)",
"Keep Tools": "Conservar herramientas",
"Keep of the other surface": "Conservar de la otra superficie",
"Knife Edge": "Borde afilado",
"LIBRARY": "BIBLIOTECA",
"LINKS": "VÍNCULOS",
"Later": "Más tarde",
@@ -199,7 +229,9 @@
"Letter Spacing": "Espaciado entre letras",
"Library": "Biblioteca",
"Library file": "Archivo de biblioteca",
"Library response is missing": "Falta la respuesta de la biblioteca",
"Line Spacing": "Interlineado",
"Line on plane": "Línea en plano",
"Local library": "Biblioteca local",
"Loop": "Bucle",
"MANAGE": "GESTIONAR",
@@ -213,9 +245,12 @@
"Material": "Material",
"Material copied. Existing body assignments are unchanged.": "Material copiado. Se conservan las asignaciones actuales de los cuerpos.",
"Max": "Máx.",
"Max Thickness": "Espesor máx.",
"Mid Radius": "Radio intermedio",
"Middle double-click": "Doble clic central",
"Min": "Mín.",
"Min Draft": "Ángulo de salida mín.",
"Min Thickness": "Espesor mín.",
"Mitre": "Inglete",
"Model Parameters": "Parámetros del modelo",
"Modeled": "Modelado",
@@ -230,6 +265,7 @@
"Name": "Nombre",
"Name on import": "Nombre al importar",
"Nearest": "Más cercano",
"Neutral plane": "Plano neutro",
"New": "Nuevo",
"New Comp.": "Nuevo comp.",
"New Sketch": "Nuevo boceto",
@@ -240,6 +276,7 @@
"No matching appearances": "No hay apariencias coincidentes",
"No matching physical materials.": "No hay materiales físicos coincidentes.",
"No visible bodies or sketches to export.": "No hay cuerpos ni bocetos visibles para exportar.",
"Nominal Radius": "Radio nominal",
"None": "Ninguno",
"Not available yet": "Todavía no disponible",
"Notch": "Muesca",
@@ -255,19 +292,25 @@
"Overall length": "Longitud total",
"Param. Graph": "Gráfico de parám.",
"Parameter": "Parámetro",
"Part length must be greater than zero and at most 2000 mm": "La longitud de la pieza debe ser mayor que cero y como máximo 2000 mm",
"Parts": "Piezas",
"Pass through": "Pasante",
"Path": "Trayectoria",
"Pattern": "Patrón",
"Per Edge": "Por arista",
"Physical material definitions": "Definiciones de materiales físicos",
"Pick a part size": "Elige un tamaño de pieza",
"Pick the tool bodies": "Selecciona los cuerpos herramienta",
"Picked": "Seleccionado",
"Picked point": "Punto seleccionado",
"Pin-slot": "Pasador-ranura",
"Place at the origin instead": "Colocar en el origen",
"Planar": "Plana",
"Plastic Rules": "Reglas de plástico",
"Playback": "Reproducción",
"Plot": "Graficar",
"Point on line": "Punto en línea",
"Point on plane": "Punto en plano",
"Position": "Posición",
"Position (0 to 1)": "Posición (de 0 a 1)",
"Position (G0)": "Posición (G0)",
@@ -277,6 +320,7 @@
"Preview leaves the current design unchanged.": "La vista previa no modifica el diseño actual.",
"Profiles/Curves": "Perfiles/curvas",
"Quantity": "Cantidad",
"Quantity must be at least 2": "La cantidad debe ser al menos 2",
"RECENT": "RECIENTES",
"REFERENCE": "REFERENCIA",
"RULED SURFACE": "SUPERFICIE REGLADA",
@@ -288,6 +332,8 @@
"Recover unsaved design?": "¿Recuperar el diseño sin guardar?",
"Rectangular": "Rectangular",
"Reference face": "Cara de referencia",
"Relief Depth": "Profundidad de alivio",
"Relief Width": "Ancho de alivio",
"Remove Tools": "Eliminar herramientas",
"Remove from list": "Quitar de la lista",
"Remove override": "Quitar anulación",
@@ -304,6 +350,7 @@
"Restore the inherited component or physical material appearance": "Restaurar la apariencia heredada del componente o del material físico",
"Result": "Resultado",
"Retake screenshot": "Repetir captura de pantalla",
"Reveal Height": "Altura del escalón",
"Reverse": "Invertir",
"Reverse Direction": "Invertir dirección",
"Revert": "Revertir",
@@ -311,7 +358,10 @@
"Revert Pos.": "Revertir pos.",
"Revert Position": "Revertir posición",
"Review the report and screenshot before opening GitHub.": "Revisa el informe y la captura antes de abrir GitHub.",
"Revolute": "Revolución",
"Rigid": "Rígida",
"Rim Edge": "Arista del borde",
"Rim face": "Cara del borde",
"Roll History Marker Here": "Desplazar marcador de la línea de tiempo hasta aquí",
"Round": "Redondo",
"Rule": "Regla",
@@ -328,11 +378,22 @@
"Search": "Buscar",
"Search commands": "Buscar comandos",
"Search this library": "Buscar en esta biblioteca",
"Second edge": "Segunda arista",
"Second point": "Segundo punto",
"Section": "Sección",
"Section position": "Posición de la sección",
"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).": "Seleccionar otro: mantén pulsado el botón izquierdo sobre el modelo (o haz clic derecho › Seleccionar otro…) para ver todo lo que hay bajo el cursor; vuelve a pulsar en el mismo lugar para seleccionar el siguiente elemento. Haz doble clic con el botón central para volver a Inicio (Preferencias › Navegación).",
"Select bodies (a face picks its body)": "Selecciona cuerpos (al seleccionar una cara se selecciona su cuerpo)",
"Select bodies or faces instead": "Selecciona cuerpos o caras en su lugar",
"Select points of the sketch being edited": "Selecciona puntos del boceto que se está editando",
"Select the centre the pattern turns around first": "Selecciona primero el centro alrededor del cual gira el patrón",
"Select the curves to offset first": "Selecciona primero las curvas que se van a desfasar",
"Select the mirror line": "Selecciona la línea de simetría",
"Select the sketch curves or points to mirror first": "Selecciona primero las curvas o puntos del boceto que se van a reflejar",
"Select the sketch curves or points to move first": "Selecciona primero las curvas o puntos del boceto que se van a mover",
"Select the sketch curves or points to pattern first": "Selecciona primero las curvas o puntos del boceto para el patrón",
"Select the sketch curves or points to scale first": "Selecciona primero las curvas o puntos del boceto que se van a escalar",
"Select the sketch curves or points to transform first": "Selecciona primero las curvas o puntos del boceto que se van a transformar",
"Selection filter": "Filtro de selección",
"Semantic faces share their appearance across instances.": "Las caras semánticas comparten su apariencia entre instancias.",
"Send your report on GitHub": "Enviar tu informe a GitHub",
@@ -350,8 +411,11 @@
"Sketch geometry and profiles": "Geometría y perfiles del boceto",
"Sketch plane has no body-face attachment.": "El plano del boceto no está vinculado a ninguna cara del cuerpo.",
"Sketch points": "Puntos del boceto",
"Slider": "Deslizante",
"Slot width": "Anchura de la ranura",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se cerró antes de guardar estos diseños. Puedes abrir su último guardado automático.",
"Spacing 2 must be a non-zero number": "El espaciado 2 debe ser un número distinto de cero",
"Spacing must be a non-zero number": "El espaciado debe ser un número distinto de cero",
"Specialise": "Especializar",
"Speed": "Velocidad",
"Square": "Cuadrado",
@@ -361,6 +425,7 @@
"Start angle": "Ángulo inicial",
"Start bulge": "Abombamiento inicial",
"Start continuity": "Continuidad inicial",
"Stationary Side": "Lado fijo",
"Step": "Paso",
"Steps": "Pasos",
"Stitch the kept pieces": "Coser las piezas conservadas",
@@ -369,29 +434,41 @@
"Style": "Estilo",
"Suppressed": "Suprimido",
"Surface": "Superficie",
"Surfaces": "Superficies",
"Swap Ends": "Intercambiar extremos",
"TRIM": "RECORTAR",
"Tangent (G1)": "Tangencia (G1)",
"Tangent Chain": "Cadena tangente",
"Target Face (optional)": "Cara de destino (opcional)",
"Tear": "Desgarro",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "El boceto discontinuo sirve de guía del plano guardado. Selecciona una cara nueva para reparar la vinculación.",
"The design changed. Reopen Change Part Size for the component.": "El diseño ha cambiado. Vuelve a abrir Cambiar tamaño de pieza para el componente.",
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "La dirección debe ser un eje o una curva recta del boceto que esté en el plano del boceto",
"The mirror line cannot also be mirrored": "La línea de simetría no puede reflejarse también",
"The mirror line must be a straight curve of this sketch, not an origin axis": "La línea de simetría debe ser una curva recta de este boceto, no un eje del origen",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Falta la instancia de autor guardada. Selecciona una posición de autor válida antes de volver a vincular.",
"Then": "Entonces",
"These features will be deleted:": "Se eliminarán estas operaciones:",
"These features will fail:": "Estas operaciones fallarán:",
"Thickness Outside": "Espesor hacia fuera",
"Thickness Variation": "Variación de espesor",
"Thin": "Delgado",
"Thin Extrude": "Extrusión delgada",
"Third plane": "Tercer plano",
"Third point": "Tercer punto",
"This child is shared. Deleting it removes it from every copy of its parent.": "Este elemento secundario es compartido. Si lo eliminas, desaparecerá de todas las copias de su elemento principal.",
"This child is shared. Position changes apply to every copy of its parent.": "Este elemento secundario es compartido. Los cambios de posición se aplican a todas las copias de su elemento principal.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta cara no permite una vinculación persistente. Elige otra cara o utiliza expresamente un plano fijo.",
"Through point": "Por un punto",
"To Offset": "Hasta desfase",
"To Snap": "Hasta el enganche",
"To edges": "Aristas de destino",
"Tolerance": "Tolerancia",
"Too close": "Demasiado cerca",
"Trimming tool (plane, face or curves)": "Herramienta de recorte (plano, cara o curvas)",
"Turn off section analysis before exporting the view.": "Desactiva el análisis de sección antes de exportar la vista.",
"Type": "Tipo",
"Type a distance or an angle to move by": "Escribe una distancia o un ángulo para mover",
"UNTRIM": "ANULAR RECORTE",
"Unit": "Unidad",
"Updating the preview": "Actualizando vista previa",
@@ -420,6 +497,17 @@
"Z angle": "Ángulo Z",
"Z distance": "Distancia Z",
"Zebra": "Cebra",
"`joint` must be an id or a name": "`joint` debe ser un id o un nombre",
"`params` must be an object": "`params` debe ser un objeto",
"`values` must be a list": "`values` debe ser una lista",
"a chamfer needs a corner of exactly two lines": "un chaflán necesita una esquina de exactamente dos líneas",
"a file operation is in progress": "hay una operación de archivo en curso",
"a picked curve is no longer in a sketch": "una curva elegida ya no está en un boceto",
"a picked curve's sketch has not been solved": "el boceto de una curva elegida no se ha resuelto",
"a picked edge has no points": "una arista elegida no tiene puntos",
"a picked edge is gone": "una arista elegida ya no existe",
"a picked edge's body is gone": "el cuerpo de una arista elegida ya no existe",
"add a key for a joint first": "añade primero una clave para una unión",
"angle, height": "ángulo, altura",
"appearance.dielectric_ior": "Índice de refracción dieléctrico",
"appearance.metallic": "Metalicidad",
@@ -428,6 +516,7 @@
"appearance.pbr_opaque": "PBR opaco",
"appearance.roughness": "Rugosidad",
"appearance.surface_response": "Respuesta de la superficie",
"bom.group_children": "Subpiezas en la fila de su pieza",
"browser.body_name": "Cuerpo{number}",
"browser.calibrate_title": "Calibrar: {name}",
"browser.capture_many": "Se han movido {count} componentes, pero sus posiciones no se han capturado.",
@@ -438,6 +527,8 @@
"browser.move_title": "Mover: {name}",
"browser.parameters": "Parámetros ({count})",
"browser.precision": "Precisión: {precision}",
"browser.sketch_attachment_lost": "{name} (fijación perdida)",
"browser.sketch_editing": "{name} (en edición)",
"browser.sketch_name": "Boceto{number}",
"browser.units": "Unidades: {units}",
"cam.flat": "Cara plana",
@@ -447,6 +538,11 @@
"cam.roller": "Rodillo",
"cam.roller_pick": "Rodillo (opcional)",
"cam.tip_pick": "Punta (opcional)",
"choose a profile": "elige un perfil",
"choose the cam's and the follower's joints": "elige las uniones de la leva y del seguidor",
"choose the joints to couple": "elige las uniones que acoplar",
"click a plane or planar face": "hacer clic en un plano o una cara plana",
"click sketch curves": "hacer clic en curvas del boceto",
"cmd.appearance_assign": "Apariencia",
"cmd.appearance_library": "Biblioteca de apariencias",
"cmd.appearance_list": "Listar apariencias",
@@ -884,6 +980,8 @@
"dxf.progress.write": "Escribiendo DXF (5/5)",
"dxf.tolerance": "Tolerancia de curva",
"dxf.tolerance_help": "Solo cuerpos visibles y bocetos mostrados. Las curvas usan segmentos rectos. Una tolerancia menor da curvas más finas y consume más tiempo y memoria.",
"each rail's curves must be in one sketch": "las curvas de cada guía deben estar en un solo boceto",
"feature computation ended without a result": "el cálculo de la operación terminó sin resultado",
"file_load.built": "Construido hasta ahora: {built} de {total} operaciones.",
"file_load.converting": "Convirtiendo sólido {done} de {total}",
"file_load.inserting": "Insertando geometría",
@@ -1078,16 +1176,55 @@
"no hole": "sin agujero",
"no interference": "sin interferencias",
"no joint that moves yet": "todavía no hay ninguna unión móvil",
"no motion study yet": "todavía no hay ningún estudio de movimiento",
"no picture": "sin imagen",
"no such joint": "no existe esa unión",
"none": "ninguno",
"not a move": "no es un movimiento",
"not a number": "no es un número",
"open a sketch first": "abre primero un boceto",
"parts.automatic_name": "automático",
"parts.dimension.across_flats": "Entrecaras",
"parts.dimension.bore": "Diámetro interior",
"parts.dimension.head_diameter": "Diámetro cabeza",
"parts.dimension.head_height": "Altura cabeza",
"parts.dimension.height": "Altura",
"parts.dimension.inner": "Diámetro interior",
"parts.dimension.outer": "Diámetro exterior",
"parts.dimension.socket": "Hexágono interior",
"parts.dimension.socket_depth": "Profundidad hexágono",
"parts.dimension.thickness": "Espesor",
"parts.dimension.width": "Anchura",
"parts.family.bearing": "Rodamiento rígido de bolas (envolvente)",
"parts.family.dowel_pin": "Pasador cilíndrico",
"parts.family.hex_bolt": "Tornillo de cabeza hexagonal",
"parts.family.hex_nut": "Tuerca hexagonal",
"parts.family.set_screw": "Prisionero con hexágono interior, punta plana",
"parts.family.socket_head_cap_screw": "Tornillo cilíndrico con hexágono interior",
"parts.family.washer": "Arandela plana",
"pattern.alignment_2": "Alineación 2",
"pattern.alignment_hint": "Los recuentos incluyen el original. Centrar equilibra las copias; Invertir elige el lado adicional cuando el total es par.",
"pattern.direction_1": "Dirección 1",
"pattern.direction_2": "Dirección 2",
"pattern.flip_direction": "Invertir dirección",
"pick a body or some of its faces": "elige un cuerpo o algunas de sus caras",
"pick a hole to seat it, or OK places it at the origin": "selecciona un agujero para colocarlo o pulsa Aceptar para situarlo en el origen",
"pick a planar face": "elige una cara plana",
"pick a plane or a face as the tool": "elige un plano o una cara como herramienta",
"pick a size": "elige un tamaño",
"pick edges of a body": "elige aristas de un cuerpo",
"pick edges of one body": "elige aristas de un solo cuerpo",
"pick edges of the holes to remove": "elige las aristas de los agujeros que quitar",
"pick faces of one body": "elige caras de un solo cuerpo",
"pick faces to offset": "elige las caras que desfasar",
"pick lines of one sketch": "elige líneas de un solo boceto",
"pick one or two vertices, edges or faces": "selecciona uno o dos vértices, aristas o caras",
"pick one plane or face as the tool, or sketch curves": "elige un plano o una cara como herramienta, o curvas de boceto",
"pick sections in two or more sketches": "elige secciones en dos o más bocetos",
"pick sketch curves or edges": "elige curvas de boceto o aristas",
"pick the face to untrim": "elige la cara a la que quitar el recorte",
"pick what to keep": "elige qué conservar",
"pick what to keep of the other surface": "elige qué conservar de la otra superficie",
"prefs.cpu": "CPU (software)",
"prefs.minutes_suffix": " min",
"prefs.theme_follow": "Seguir un archivo de tema",
@@ -1720,6 +1857,26 @@
"search.suggest": "¿Quisiste decir {label}?",
"search.try": "Escribe el nombre de un comando, una operación de este diseño o un parámetro. Para ejecutar un comando con valores, escribe su id y un objeto JSON: {example}",
"search.valid": "Válido",
"select a direction (an axis, a sketch line or a straight edge) first": "selecciona primero una dirección (un eje, una línea de boceto o una arista recta)",
"select a direction (an axis, a sketch line or a straight edge) for the profile to follow": "selecciona una dirección (un eje, una línea de boceto o una arista recta) que seguirá el perfil",
"select a point on a component": "selecciona un punto en un componente",
"select a profile, edges or sketch lines first": "selecciona primero un perfil, aristas o líneas de boceto",
"select an axis": "selecciona un eje",
"select an axis, a straight or circular edge, or a cylinder, cone or torus face first": "selecciona primero un eje, una arista recta o circular, o una cara cilíndrica, cónica o toroidal",
"select bodies first": "selecciona primero cuerpos",
"select corner points, or the two lines of one corner": "selecciona puntos de esquina, o las dos líneas de una esquina",
"select one corner point, or the two lines of one corner": "selecciona un punto de esquina, o las dos líneas de una esquina",
"select sheet edges first": "selecciona primero aristas de chapa",
"select the bend line (a sketch line) first": "selecciona primero la línea de plegado (una línea de boceto)",
"select the cam's profile (a sketch profile or the cam's edges)": "selecciona el perfil de la leva (un perfil de boceto o las aristas de la leva)",
"select the corner to cut first": "selecciona primero la esquina que cortar",
"select the corners to round first": "selecciona primero las esquinas que redondear",
"select the fixed face first (a plate's large face)": "selecciona primero la cara fija (la cara grande de una placa)",
"select the frame sketch's lines first": "selecciona primero las líneas del boceto del marco",
"select the path (sketch lines across the edge)": "selecciona la trayectoria (líneas de boceto a lo largo de la arista)",
"select the rim face first": "selecciona primero la cara del borde",
"select the sheet edge first": "selecciona primero la arista de la chapa",
"select two components": "selecciona dos componentes",
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista.",
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
@@ -1732,16 +1889,22 @@
"settings.reset_intro": "Estos ajustes no funcionan en este ordenador e impedirían iniciar SolidCraft, por lo que se han cambiado:",
"settings.reset_title": "Se han cambiado los ajustes",
"settings.supersampling_note": "Dibuja la vista a mayor resolución y después la reduce: bordes, líneas y cuadrícula más suaves en pantallas inferiores a 4K. Consume recursos de GPU: unos 2,25× píxeles a 1,5× y 4× a 2×.",
"sheet.flange_type_base": "Base",
"sheet.flange_type_contour": "Contorno",
"sheet.flange_type_edge": "Arista",
"shortcut.json_error": "los parámetros deben estar en formato JSON: {error}",
"shortcut.no_command": "no existe el comando `{id}`; el texto anterior al JSON debe ser un ID de comando",
"shortcut.pin": "Fijar en la parte superior del cuadro S",
"shortcut.unpin": "Desfijar",
"sketch is missing": "falta el boceto",
"sketch.degrees_of_freedom": "{count} grados de libertad",
"sketch.dof_left": "Quedan {count} grados de libertad",
"sketch.dof_left_one": "Queda {count} grado de libertad",
"sketch.draw_curves": "Dibujar como {linetype}",
"sketch.make_selection": "Convertir la selección a {linetype}",
"sketch.open_ends": "Abiertos {count}",
"sketch.status_conflict": "restricciones en conflicto",
"sketch.status_fully_constrained": "totalmente restringido",
"sketch.untied_ends": "Sin vincular {count}",
"sketch_panel.attached": "Vinculado a {body} / {path}",
"sketch_panel.author": "Ubicación de autoría: {path}",
@@ -1763,7 +1926,24 @@
"sketch_text.path_hint": "Curva del boceto que seguirá el texto: haz clic aquí y después en la curva",
"sketch_text.use_font_file": "Usar el archivo de fuente {path}",
"solid": "sólido",
"the angle must be a number": "el ángulo debe ser un número",
"the curves must be in one sketch": "las curvas deben estar en un solo boceto",
"the direction must be an axis, a sketch line or a straight edge": "la dirección debe ser un eje, una línea de boceto o una arista recta",
"the duration must be a number of seconds, 0.001 or more": "la duración debe ser un número de segundos, 0.001 o más",
"the line's sketch is gone": "el boceto de la línea ya no existe",
"the lines must be in one sketch": "las líneas deben estar en un solo boceto",
"the lines must be in one sketch, not the section's": "las líneas deben estar en un solo boceto, no en el de la sección",
"the parts library is empty": "la biblioteca de piezas está vacía",
"the parts library is not available": "la biblioteca de piezas no está disponible",
"the path curves must be in one other sketch": "las curvas de la trayectoria deben estar en un solo boceto distinto",
"the path's sketch is gone": "el boceto de la trayectoria ya no existe",
"the position must be a number": "la posición debe ser un número",
"the preview failed": "falló la vista previa",
"the profiles and curves must be in one sketch": "los perfiles y las curvas deben estar en un solo boceto",
"the reference face must be on the edges' body": "la cara de referencia debe estar en el cuerpo de las aristas",
"the scale must be above 0": "la escala debe ser mayor que 0",
"the section's profiles must be in one sketch": "los perfiles de la sección deben estar en un solo boceto",
"the two origins are on the same component": "los dos orígenes están en el mismo componente",
"thread.automatic_size": "Tamaño automático",
"thread.designation": "Designación",
"thread.iso_metric": "Métrica ISO",
@@ -1790,6 +1970,7 @@
"trace.points": "{count} puntos a lo largo de la trayectoria",
"trace.stopped_by_contact": "Termina en la muestra {sample}: {a} atravesaría {b}",
"trace.stopped_by_loop": "Termina en la muestra {sample}: el bucle que pasa por {joint} no puede cerrarse más",
"trim by sketch curves, or by one plane or face": "recorta con curvas de boceto, o con un plano o una cara",
"ui.1_degree_of_freedom": "1 grado de libertad",
"ui.3d_mouse_axes_reassigned": "Los ejes están reasignados en Preferencias, y eso sigue aplicándose sobre esta calibración.",
"ui.3d_mouse_button": "Botón {number}",
@@ -2216,6 +2397,8 @@
"ui.design_properties.revision": "Revisión",
"ui.diameter": "Diámetro",
"ui.dimension": "Cota",
"ui.direct_interaction": "Interacción directa",
"ui.direct_interaction_note": "Trabaja directamente sobre el modelo en la vista. Pulsa una cara plana o un perfil y tira: hacia arriba extruye, hacia abajo dentro de un cuerpo corta, y el movimiento lateral fija el paso",
"ui.direction": "Dirección",
"ui.display_quality": "Calidad de visualización",
"ui.display_quality_coarse": "Baja",
@@ -2239,6 +2422,7 @@
"ui.editJointLimits": "Editar límites de unión",
"ui.edit_section_analysis": "Editar análisis de sección",
"ui.ellipse": "Elipse",
"ui.emboss_wrap_note": "Los perfiles de líneas ortogonales tangentes a un cilindro compatible se envuelven automáticamente. Elegir una pared para definir el destino.",
"ui.end_radius": "Radio final",
"ui.end_scale": "Escala final",
"ui.entities": "Entidades",
@@ -2304,6 +2488,7 @@
"ui.heading_extrude": "EXTRUIR",
"ui.heading_fillet": "REDONDEO",
"ui.heading_finish_sketch": "FINALIZAR BOCETO",
"ui.heading_form": "FORMA",
"ui.heading_front": "FRONTAL",
"ui.heading_hole": "AGUJERO",
"ui.heading_home": "INICIO",
@@ -2352,6 +2537,7 @@
"ui.heading_utilities": "UTILIDADES",
"ui.heading_utility": "UTILIDAD",
"ui.heading_weldments": "SOLDADURA",
"ui.height": "Altura",
"ui.help": "Ayuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar ejes",
@@ -2379,11 +2565,14 @@
"ui.intersect": "Intersecar",
"ui.isolate": "Aislar",
"ui.join": "Unir",
"ui.joint_play": "▶ Reproducir",
"ui.joint_stop": "■ Detener",
"ui.joints": "Uniones",
"ui.keyboard_shortcuts": "Atajos de teclado",
"ui.language": "Idioma",
"ui.layout_compact": "Compacta",
"ui.layout_floating": "Flotante",
"ui.length": "Longitud",
"ui.length_decimals": "Decimales de longitud",
"ui.light": "Claro",
"ui.line": "Línea",
@@ -2391,6 +2580,7 @@
"ui.loft": "Solevación",
"ui.look_at": "Mirar hacia",
"ui.look_at_anim_desc": "Vista de eje, Mirar hacia, Inicio y Ajustar",
"ui.major_radius": "Radio mayor",
"ui.manipulator_free_alt": "Arrastre libre del manipulador también con Alt/Option",
"ui.manipulator_free_alt_tip": "Activado: Alt/Option o Ctrl/Cmd sobre las flechas de extrusión / manipulador desactiva el ajuste de valores. Desactivado: solo Ctrl/Cmd.",
"ui.manufacture": "Fabricación",
@@ -2420,8 +2610,10 @@
"ui.mesh_format_3mf": "3MF",
"ui.mesh_format_3mf_tagged": "3MF con etiquetas de cara",
"ui.mesh_format_stl": "STL",
"ui.minor_radius": "Radio menor",
"ui.mirror": "Simetría",
"ui.mirror_line": "Línea de simetría",
"ui.modifiers_either": "{first} o {second}",
"ui.move": "Mover",
"ui.multiple": "Múltiple",
"ui.named_views": "Vistas guardadas",
@@ -2520,6 +2712,7 @@
"ui.reverse_horizontal_pan_direction": "Invertir dirección del desplazamiento horizontal",
"ui.reverse_horizontal_pan_tip": "El desplazamiento horizontal con dos dedos y la rueda de pulgar desplazan en sentido contrario",
"ui.reverse_zoom_direction": "Invertir dirección del zoom",
"ui.revolutions": "Vueltas",
"ui.revolve": "Revolución",
"ui.ribbon_exit": "Salir",
"ui.ribbon_labels": "Mostrar texto bajo los iconos de la cinta",
@@ -2535,6 +2728,7 @@
"ui.scale": "Escala",
"ui.second_rail": "Segunda línea guía",
"ui.section_analysis": "Análisis de sección",
"ui.section_size": "Tamaño de la sección",
"ui.select": "Seleccionar",
"ui.selectOther": "Seleccionar otro…",
"ui.selectTangentChain": "Seleccionar cadena tangente",
@@ -2585,6 +2779,7 @@
"ui.start": "Iniciar",
"ui.start_over": "Volver a empezar",
"ui.start_page": "Página de inicio",
"ui.stitch_tolerance_hint": "estricta",
"ui.stretch": "Estirar",
"ui.supersampling": "Supermuestreo (SSAA)",
"ui.supersampling_adaptive": "Solo cuando la vista está inmóvil",
@@ -2615,6 +2810,8 @@
"ui.to_object": "Hasta objeto",
"ui.tool_direction": "Dirección de herramienta",
"ui.top": "Superior",
"ui.top_diameter": "Diámetro superior",
"ui.top_radius": "Radio superior",
"ui.torus": "Toro",
"ui.total_angle": "Ángulo total",
"ui.trackpad_pan_tip": "Los gestos del panel táctil desplazan la vista (desactivado: hacen zoom como la rueda); la rueda del ratón hace zoom, la rueda lateral desplaza horizontalmente y el gesto de pellizco siempre hace zoom",
@@ -2640,6 +2837,7 @@
"ui.visual_shaded_with_edges": "Sombreado con aristas",
"ui.visual_style": "Estilo visual",
"ui.visual_wireframe": "Estructura alámbrica",
"ui.width": "Anchura",
"ui.workspace": "espacio de trabajo",
"ui.workspace_animation_description": "Vistas explosionadas, estudios de movimiento y guiones gráficos",
"ui.workspace_available": "Disponible",
@@ -2695,5 +2893,8 @@
"update.up_to_date": "SolidCraft está actualizado.",
"update.update_now": "Actualizar ahora",
"update.you_have": "Tienes la versión {version}.",
"use a positive nominal diameter and a pitch smaller than it": "usa un diámetro nominal positivo y un paso menor que él",
"values must be finite numbers": "los valores deben ser números finitos",
"values must be numbers or expressions": "los valores deben ser números o expresiones",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ elegir · Intro / Tab completar · Esc cerrar"
}
+206
View File
@@ -1,4 +1,5 @@
{
"(active rule)": "(règle active)",
"0 turns it off": "0 le désactive",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Ouvrir le brouillon GitHub ci-dessous, puis coller le rapport copié dans le presse-papiers.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Ouvrir le brouillon GitHub ci-dessous. Votre titre et votre rapport y seront déjà remplis.",
@@ -35,6 +36,7 @@
"All open designs": "Toutes les conceptions ouvertes",
"Allowed expressions, separated by semicolons": "Expressions autorisées, séparées par des points-virgules",
"Also deletes": "Supprime aussi",
"Also place them from the origin (fully constrained)": "Les placer aussi depuis l'origine (entièrement contraintes)",
"Anchor": "Ancrage",
"Angle 2": "Angle 2",
"Angle Display": "Affichage de l'angle",
@@ -49,15 +51,19 @@
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Appliquez d’abord votre apparence. Exporte les apparences attribuées et les matériaux créés dans cette conception.",
"Arc length": "Longueur d'arc",
"Area": "Aire",
"As Picked": "Tel que sélectionné",
"As-Built": "Tel que construit",
"Assign Rule": "Attribuer une règle",
"Attachment": "Rattachement",
"BLEND SURFACE": "SURFACE DE RACCORD",
"BOUNDARY SURFACE": "SURFACE FRONTIÈRE",
"BROWSER": "NAVIGATEUR",
"Ball": "Rotule",
"Bar": "Plat",
"Base Height": "Hauteur de la base",
"Bend": "Angle de pli",
"Bend Edges": "Arêtes de pli",
"Bend Line": "Ligne de pli",
"Bend Line Position": "Position de la ligne de pli",
"Bend Position": "Position du pli",
"Bend Radius": "Rayon de pliage",
@@ -69,6 +75,7 @@
"Body (last if none)": "Corps (le dernier si aucun)",
"Body to split": "Corps à scinder",
"Body, or faces to take out": "Corps, ou faces à retirer",
"Both": "Les deux",
"Bottom Left": "Bas gauche",
"Bottom Middle": "Bas centre",
"Bottom Right": "Bas droite",
@@ -100,6 +107,7 @@
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choisir un nouveau plan. La géométrie et les cotes de l'esquisse restent inchangées dans ses coordonnées bidimensionnelles.",
"Choose…": "Choisir…",
"Circle center": "Centre du cercle",
"Circle or sphere": "Cercle ou sphère",
"Circular": "Circulaire",
"Circumference": "Circonférence",
"Clockwise": "Horaire",
@@ -116,6 +124,7 @@
"Config Table": "Table de config.",
"Configuration": "Configuration",
"Configuration rules": "Règles de configuration",
"Constant radius": "Rayon constant",
"Contact": "Contact",
"Contact Sets": "Jeux de contacts",
"Contact sets": "Jeux de contact",
@@ -150,7 +159,9 @@
"Custom": "Personnalisé",
"Customize Toolbar": "Personnaliser la barre d'outils",
"Customize…": "Personnaliser…",
"Cylindrical": "Pivot glissant",
"Cylindrical face": "Face cylindrique",
"Deboss": "Embossage en creux",
"Delete": "Supprimer",
"Delete Selected Features…": "Supprimer les fonctions sélectionnées…",
"Delete Selected Folders": "Supprimer les dossiers sélectionnés",
@@ -165,6 +176,7 @@
"Designs you open or save appear here.": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Déterminé par Jusqu'à l'objet. Choisir Angle pour modifier l'étendue angulaire.",
"Diameter": "Diamètre",
"Dimension new shapes": "Coter les nouvelles formes",
"Direction": "Direction",
"Discard": "Abandonner",
"Distance": "Distance",
@@ -176,6 +188,9 @@
"Drag to move the hole; click to remove it": "Faire glisser pour déplacer le perçage ; cliquer pour le retirer",
"Drive Joint": "Piloter la liaison",
"Duration": "Durée",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Chaque nouvelle forme est aussi cotée depuis l'origine de l'esquisse jusqu'à ce que rien ne reste libre. Une forme déjà liée à l'origine ou à une géométrie placée n'en reçoit aucune.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Chaque forme dessinée reçoit toutes ses cotes aux tailles dessinées (les valeurs saisies l'emportent). Une taille déjà fixée par l'esquisse est omise.",
"Edge": "Arête",
"Edges": "Arêtes",
"Edit": "Modifier",
"Edit Profile Sketch": "Modifier l'esquisse du profil",
@@ -184,6 +199,7 @@
"End Radius": "Rayon final",
"End bulge": "Renflement de fin",
"End continuity": "Continuité de fin",
"Equal Distance": "Distance égale",
"Expand": "Développer",
"Expected result (optional)": "Résultat attendu (facultatif)",
"Explode": "Éclater",
@@ -211,6 +227,8 @@
"Faces": "Faces",
"Faces / edges / vertices": "Faces / arêtes / sommets",
"Faces to split": "Faces à scinder",
"Faces/Body": "Faces/Corps",
"Factor must be a positive number": "Le facteur doit être un nombre strictement positif",
"Fastener": "Fixation",
"Favorites": "Favoris",
"Favourite": "Favori",
@@ -220,9 +238,12 @@
"Filter…": "Filtrer…",
"Find in Browser": "Trouver dans le navigateur",
"Finish": "Terminer",
"First edge": "Première arête",
"First plane": "Premier plan",
"First point": "Premier point",
"Fit and G2 corners require Constant radius.": "L'ajustement et les angles G2 nécessitent un rayon constant.",
"Fixed Face": "Face fixe",
"Flange Gap": "Jeu de bord tombé",
"Flange thickness": "Épaisseur d'aile",
"Flanges": "Bords tombés",
"Flat Pattern": "Dépliage",
@@ -230,8 +251,10 @@
"Follow camera (Look At sketches)": "Suivre la caméra (vue normale aux esquisses)",
"Follow the camera (Look At sketches)": "Suivre la caméra (vue normale aux esquisses)",
"Font": "Police",
"Free Move": "Déplacement libre",
"From": "De",
"From Bodies": "À partir de corps",
"From Snap": "Depuis l'accrochage",
"From edges": "Arêtes de départ",
"Full Length": "Longueur totale",
"Full thread depth": "Profondeur de filetage complète",
@@ -243,10 +266,13 @@
"Halo under sketch curves": "Halo sous les courbes d'esquisse",
"Height": "Hauteur",
"Height From": "Hauteur depuis",
"Hem Gap": "Jeu d'ourlet",
"Hidden edges": "Arêtes cachées",
"History layout": "Disposition de l’historique",
"History names": "Noms dans l'historique",
"Hole (optional)": "Perçage (facultatif)",
"Hole Depth": "Profondeur du trou",
"Hole Diameter": "Diamètre du trou",
"Holes": "Trous",
"Holes (edges on them)": "Trous (par leurs arêtes)",
"Home view": "Vue d'accueil",
@@ -269,12 +295,16 @@
"Issue title": "Titre du ticket",
"Items": "Éléments",
"JOINTS": "LIAISONS",
"Joint 1": "Liaison 1",
"Joint 2": "Liaison 2",
"Joint Origin1": "Origine de liaison1",
"K Factor": "Facteur K",
"Keep (points on it)": "À conserver (points dessus)",
"Keep Tools": "Conserver les outils",
"Keep of the other surface": "À conserver de l'autre surface",
"Key a joint's value at this step": "Placer une image clé de la valeur d'une liaison à cette étape",
"Keys": "Images clés",
"Knife Edge": "Arête vive",
"L-Angle": "Cornière",
"LIBRARY": "BIBLIOTHÈQUE",
"LINKS": "LIENS",
@@ -287,9 +317,11 @@
"Letter Spacing": "Espacement des lettres",
"Library": "Bibliothèque",
"Library file": "Fichier de bibliothèque",
"Library response is missing": "La réponse de la bibliothèque est manquante",
"Limits": "Limites",
"Line Spacing": "Interligne",
"Line Type": "Type de ligne",
"Line on plane": "Ligne sur plan",
"Lines": "Lignes",
"Listing the installed fonts…": "Liste des polices installées…",
"Load library": "Charger la bibliothèque",
@@ -309,11 +341,14 @@
"Material Outside": "Matière à l'extérieur",
"Material copied. Existing body assignments are unchanged.": "Matériau copié. Les affectations existantes des corps sont inchangées.",
"Max": "Max",
"Max Thickness": "Épaisseur max.",
"Maximum": "Maximum",
"Mesh body": "Corps maillé",
"Mid Radius": "Rayon intermédiaire",
"Middle double-click": "Double-clic milieu",
"Min": "Min",
"Min Draft": "Dépouille min.",
"Min Thickness": "Épaisseur min.",
"Minimum": "Minimum",
"Mirror Plane": "Plan de symétrie",
"Mitre": "Onglet",
@@ -333,6 +368,7 @@
"Name": "Nom",
"Name on import": "Nom à l'importation",
"Nearest": "Corps le plus proche",
"Neutral plane": "Plan neutre",
"New": "Nouveau",
"New Comp.": "Nouv. comp.",
"New Sketch": "Nouvelle esquisse",
@@ -343,6 +379,7 @@
"No matching appearances": "Aucune apparence correspondante",
"No matching physical materials.": "Aucun matériau physique correspondant.",
"No visible bodies or sketches to export.": "Aucun corps ni esquisse visible à exporter.",
"Nominal Radius": "Rayon nominal",
"Nominal diameter": "Diamètre nominal",
"None": "Aucun",
"Not available yet": "Pas encore disponible",
@@ -371,22 +408,28 @@
"Overall length": "Longueur hors tout",
"Param. Graph": "Graphe param.",
"Parameter": "Paramètre",
"Part length must be greater than zero and at most 2000 mm": "La longueur de la pièce doit être supérieure à zéro et d'au plus 2000 mm",
"Parts": "Pièces",
"Pass through": "Passer par",
"Path": "Trajectoire",
"Pattern": "Répétition",
"Per Edge": "Par arête",
"Physical material definitions": "Définitions de matériaux physiques",
"Pick a part size": "Choisir une taille de pièce",
"Pick the tool bodies": "Choisir les corps outils",
"Picked": "Sélectionnés",
"Picked point": "Point choisi",
"Pin-slot": "Axe-rainure",
"Pitch": "Pas",
"Pivot": "Pivot",
"Place at the origin instead": "Placer plutôt à l'origine",
"Planar": "Plan",
"Plastic Rules": "Règles plastique",
"Play the joint's motion in the view": "Lire le mouvement de la liaison dans la vue",
"Playback": "Lecture",
"Plot": "Graphique",
"Point on line": "Point sur ligne",
"Point on plane": "Point sur plan",
"Position": "Position",
"Position (0 to 1)": "Position (0 à 1)",
"Position (G0)": "Position (G0)",
@@ -399,6 +442,7 @@
"Profiles or curves": "Profils ou courbes",
"Profiles/Curves": "Profils/Courbes",
"Quantity": "Quantité",
"Quantity must be at least 2": "La quantité doit être d'au moins 2",
"RECENT": "RÉCENTS",
"REFERENCE": "RÉFÉRENCE",
"RULED SURFACE": "SURFACE RÉGLÉE",
@@ -412,6 +456,8 @@
"Rectangular": "Rectangulaire",
"Rectangular Tube": "Tube rectangulaire",
"Reference face": "Face de référence",
"Relief Depth": "Profondeur de dégagement",
"Relief Width": "Largeur de dégagement",
"Remove Tools": "Retirer les outils",
"Remove condition": "Supprimer la condition",
"Remove from Folder": "Retirer du dossier",
@@ -437,6 +483,7 @@
"Restore this studio's tools": "Restaurer les outils de ce studio",
"Result": "Résultat",
"Retake screenshot": "Reprendre la capture d'écran",
"Reveal Height": "Hauteur de décroché",
"Reverse": "Inverser",
"Reverse Direction": "Inverser la direction",
"Revert": "Rétablir",
@@ -445,12 +492,15 @@
"Revert Position": "Rétablir la position",
"Review the report and screenshot before opening GitHub.": "Relisez le rapport et la capture d'écran avant d'ouvrir GitHub.",
"Review your report on GitHub and click Submit new issue to publish it.": "Vérifiez votre rapport sur GitHub et cliquez sur Submit new issue pour le publier.",
"Revolute": "Pivot",
"Rib Length": "Longueur de nervure",
"Rib Offset": "Décalage de nervure",
"Rib Thickness": "Épaisseur de nervure",
"Ribs": "Nervures",
"Right Middle": "Milieu droite",
"Rigid": "Rigide",
"Rim Edge": "Arête du bord",
"Rim face": "Face de bord",
"Ring of tools on the right button": "Anneau d'outils sur le bouton droit",
"Roll Back Here": "Revenir ici dans l'historique",
"Roll History Marker Here": "Placer le marqueur d'historique ici",
@@ -475,7 +525,9 @@
"Search": "Rechercher",
"Search commands": "Rechercher des commandes",
"Search this library": "Rechercher dans cette bibliothèque",
"Second edge": "Deuxième arête",
"Second plane": "Second plan",
"Second point": "Deuxième point",
"Section": "Coupe",
"Section (optional)": "Section (facultative)",
"Section position": "Position de la section",
@@ -485,6 +537,15 @@
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
"Select edges": "Sélectionner des arêtes",
"Select points of the sketch being edited": "Sélectionner des points de l'esquisse en cours de modification",
"Select the centre the pattern turns around first": "Sélectionner d'abord le centre autour duquel la répétition pivote",
"Select the curves to offset first": "Sélectionner d'abord les courbes à décaler",
"Select the mirror line": "Sélectionner la ligne de symétrie",
"Select the sketch curves or points to mirror first": "Sélectionner d'abord les courbes ou points d'esquisse à symétriser",
"Select the sketch curves or points to move first": "Sélectionner d'abord les courbes ou points d'esquisse à déplacer",
"Select the sketch curves or points to pattern first": "Sélectionner d'abord les courbes ou points d'esquisse à répéter",
"Select the sketch curves or points to scale first": "Sélectionner d'abord les courbes ou points d'esquisse à mettre à l'échelle",
"Select the sketch curves or points to transform first": "Sélectionner d'abord les courbes ou points d'esquisse à transformer",
"Selection filter": "Filtre de sélection",
"Semantic faces share their appearance across instances.": "Les faces sémantiques partagent leur apparence entre les instances.",
"Send your report on GitHub": "Envoyer votre rapport sur GitHub",
@@ -504,9 +565,12 @@
"Sketch geometry and profiles": "Géométrie et profils d'esquisse",
"Sketch plane has no body-face attachment.": "Le plan d'esquisse n'est rattaché à aucune face de corps.",
"Sketch points": "Points d'esquisse",
"Slider": "Glissière",
"Slot width": "Largeur de rainure",
"Snap to face and edge centres": "Accrocher aux centres des faces et des arêtes",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
"Spacing 2 must be a non-zero number": "L'espacement 2 doit être un nombre non nul",
"Spacing must be a non-zero number": "L'espacement doit être un nombre non nul",
"Specialise": "Spécialiser",
"Speed": "Vitesse",
"Splitting tool": "Outil de scission",
@@ -517,6 +581,7 @@
"Start angle": "Angle de départ",
"Start bulge": "Renflement de début",
"Start continuity": "Continuité de début",
"Stationary Side": "Côté fixe",
"Step": "Étape",
"Steps": "Étapes",
"Steps to reproduce (optional)": "Étapes pour reproduire le problème (facultatives)",
@@ -530,6 +595,7 @@
"Suppressed": "Désactivé",
"Surface": "Surface",
"Surface, or faces to turn over": "Surface, ou faces à retourner",
"Surfaces": "Surfaces",
"Swap Ends": "Permuter les extrémités",
"TRIM": "AJUSTER",
"Tangent": "Tangence",
@@ -540,24 +606,34 @@
"Tap drill diameter": "Diamètre de l'avant-trou",
"Target": "Cible",
"Target Body": "Corps cible",
"Target Face (optional)": "Face cible (facultatif)",
"Tear": "Déchirure",
"Technical details": "Détails techniques",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "L'esquisse en pointillés est un guide de plan enregistré. Sélectionnez une nouvelle face pour réparer le rattachement.",
"The design changed. Reopen Change Part Size for the component.": "La conception a changé. Rouvrir Changer la taille de la pièce pour le composant.",
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "La direction doit être un axe ou une courbe d'esquisse droite située dans le plan de l'esquisse",
"The mirror line cannot also be mirrored": "La ligne de symétrie ne peut pas être elle-même symétrisée",
"The mirror line must be a straight curve of this sketch, not an origin axis": "La ligne de symétrie doit être une courbe droite de cette esquisse, et non un axe de l'origine",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "L'occurrence d'auteur enregistrée est introuvable. Sélectionnez un placement d'auteur valide avant de relier.",
"Then": "Alors",
"These features will be deleted:": "Ces fonctions seront supprimées :",
"These features will fail:": "Ces fonctions échoueront :",
"Thickness Outside": "Épaisseur extérieure",
"Thickness Variation": "Variation d'épaisseur",
"Thin": "Mince",
"Thin Extrude": "Extrusion mince",
"Third plane": "Troisième plan",
"Third point": "Troisième point",
"This child is shared. Deleting it removes it from every copy of its parent.": "Cet enfant est partagé. Le supprimer le retire de toutes les copies de son parent.",
"This child is shared. Position changes apply to every copy of its parent.": "Cet enfant est partagé. Les changements de position s'appliquent à toutes les copies de son parent.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Cette face ne peut pas fournir de rattachement durable. Choisissez une autre face ou utilisez explicitement un plan fixe.",
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Cette image n'est pas encore jointe sur GitHub. Copiez-la ou enregistrez-la, puis joignez-la sur GitHub.",
"Thread depth": "Profondeur de filetage",
"Thread diameter": "Diamètre de filetage",
"Through point": "Par un point",
"To": "À",
"To Offset": "Jusqu'au décalage",
"To Snap": "Vers l'accrochage",
"To edges": "Arêtes d'arrivée",
"Toe radius": "Rayon d'extrémité",
"Tolerance": "Tolérance",
@@ -571,6 +647,7 @@
"Trimming tool (plane, face or curves)": "Outil d'ajustement (plan, face ou courbes)",
"Turn off section analysis before exporting the view.": "Désactivez l'analyse de coupe avant d'exporter la vue.",
"Type": "Type",
"Type a distance or an angle to move by": "Saisir une distance ou un angle de déplacement",
"UNTRIM": "ANNULER L'AJUSTEMENT",
"Unit": "Unité",
"Unsuppress": "Réactiver",
@@ -614,6 +691,17 @@
"Z angle": "Angle Z",
"Z distance": "Distance Z",
"Zebra": "Zébrures",
"`joint` must be an id or a name": "`joint` doit être un identifiant ou un nom",
"`params` must be an object": "`params` doit être un objet",
"`values` must be a list": "`values` doit être une liste",
"a chamfer needs a corner of exactly two lines": "un chanfrein nécessite un coin formé d'exactement deux lignes",
"a file operation is in progress": "une opération sur un fichier est en cours",
"a picked curve is no longer in a sketch": "une courbe sélectionnée ne se trouve plus dans une esquisse",
"a picked curve's sketch has not been solved": "l'esquisse d'une courbe sélectionnée n'a pas été résolue",
"a picked edge has no points": "une arête sélectionnée n'a aucun point",
"a picked edge is gone": "une arête sélectionnée n'existe plus",
"a picked edge's body is gone": "le corps d'une arête sélectionnée n'existe plus",
"add a key for a joint first": "ajouter d'abord une clé pour une liaison",
"ai.connected_toast": "SolidCraft est maintenant disponible dans {clients}. Redémarrez-les pour l'utiliser (Paramètres > Assistant).",
"ai.prefs.allow_control": "Autoriser la connexion des agents IA (canal de contrôle / MCP)",
"ai.prefs.allow_control_note": "Ouvre un canal de contrôle limité à cet ordinateur (127.0.0.1, port 27878 ou le suivant disponible). Un programme qui se connecte doit aussi présenter un jeton secret lisible uniquement par votre compte utilisateur, régénéré à chaque démarrage. Prend effet au prochain démarrage de SolidCraft. Désactivé : rien n'écoute, sauf si vous lancez SolidCraft avec --control.",
@@ -650,6 +738,7 @@
"appearance.pbr_opaque": "PBR opaque",
"appearance.roughness": "Rugosité",
"appearance.surface_response": "Réponse de la surface",
"bom.group_children": "Sous-pièces dans la ligne de leur pièce",
"browser.body_name": "Corps{number}",
"browser.calibrate_title": "Calibrer : {name}",
"browser.capture_many": "{count} composants ont été déplacés, mais leurs positions ne sont pas capturées.",
@@ -660,6 +749,8 @@
"browser.move_title": "Déplacer : {name}",
"browser.parameters": "Paramètres ({count})",
"browser.precision": "Précision : {precision}",
"browser.sketch_attachment_lost": "{name} (attache perdue)",
"browser.sketch_editing": "{name} (en cours de modification)",
"browser.sketch_name": "Esquisse{number}",
"browser.units": "Unités : {units}",
"cam.flat": "Plateau",
@@ -669,6 +760,11 @@
"cam.roller": "Galet",
"cam.roller_pick": "Galet (facultatif)",
"cam.tip_pick": "Pointe (facultatif)",
"choose a profile": "choisir un profilé",
"choose the cam's and the follower's joints": "choisir les liaisons de la came et du suiveur",
"choose the joints to couple": "choisir les liaisons à coupler",
"click a plane or planar face": "cliquer sur un plan ou une face plane",
"click sketch curves": "cliquer sur des courbes d'esquisse",
"cmd.appearance_assign": "Apparence",
"cmd.appearance_library": "Bibliothèque d'apparences",
"cmd.appearance_list": "Lister les apparences",
@@ -1139,6 +1235,8 @@
"dxf.progress.write": "Écriture du DXF (5/5)",
"dxf.tolerance": "Tolérance des courbes",
"dxf.tolerance_help": "Corps visibles et esquisses affichées uniquement. Les courbes sont approchées par des segments de droite. Une tolérance plus faible donne des courbes plus fines, au prix de plus de temps et de mémoire.",
"each rail's curves must be in one sketch": "les courbes de chaque rail de guidage doivent être dans une seule esquisse",
"feature computation ended without a result": "le calcul de la fonction s'est terminé sans résultat",
"file.open.drawing_filter": "Conceptions, fichiers CAO, maillages et dessins",
"file_load.built": "Construit jusqu’ici : {built} entités sur {total}.",
"file_load.converting": "Conversion du solide {done} sur {total}",
@@ -1356,7 +1454,9 @@
"nav.free_orbit.help": "La souris et la souris 3D tournent en passant par les deux pôles et conservent le roulis. L'alignement des axes avec Maj est désactivé.",
"nav.free_orbit.label": "Orbite libre",
"nav.fusion.help": "Glisser avec le milieu fait un panoramique, Maj+milieu fait orbiter, glisser avec le droit fait orbiter",
"nav.hfss.help": "Alt+glisser gauche fait un panoramique, Maj+Alt+gauche zoome, Ctrl+glisser gauche sur un espace vide fait orbiter",
"nav.inventor.help": "Glisser avec le milieu fait un panoramique, Maj+milieu fait orbiter, Ctrl+milieu zoome, glisser avec le droit fait orbiter",
"nav.maya.help": "Alt+glisser gauche fait orbiter, Alt+milieu fait un panoramique, Alt+droit zoome ; glisser avec le milieu fait aussi un panoramique",
"nav.openscad.help": "Glisser avec le gauche dans le vide fait orbiter, Maj+glisser gauche sélectionne par rectangle, glisser la géométrie d'esquisse la modifie ; glisser avec le droit ou le milieu fait un panoramique, Maj+droit ou Maj+milieu zoome",
"nav.solidworks.help": "Glisser avec le milieu fait orbiter, Ctrl+milieu fait un panoramique, Maj+milieu zoome, glisser avec le droit fait orbiter",
"nav.tinkercad.help": "Glisser avec le droit fait orbiter, glisser avec le milieu ou Maj+droit fait un panoramique",
@@ -1372,9 +1472,34 @@
"no hole": "sans perçage",
"no interference": "aucune interférence",
"no joint that moves yet": "aucune liaison mobile pour l'instant",
"no motion study yet": "pas encore d'étude de mouvement",
"no picture": "aucune image",
"no such joint": "liaison introuvable",
"none": "aucun",
"not a move": "pas un déplacement",
"not a number": "pas un nombre",
"open a sketch first": "ouvrir d'abord une esquisse",
"parts.automatic_name": "automatique",
"parts.dimension.across_flats": "Sur plats",
"parts.dimension.bore": "Alésage",
"parts.dimension.d": "Diamètre nominal",
"parts.dimension.head_diameter": "Diamètre de tête",
"parts.dimension.head_height": "Hauteur de tête",
"parts.dimension.height": "Hauteur",
"parts.dimension.inner": "Diamètre intérieur",
"parts.dimension.outer": "Diamètre extérieur",
"parts.dimension.pitch": "Pas",
"parts.dimension.socket": "Empreinte",
"parts.dimension.socket_depth": "Profondeur empreinte",
"parts.dimension.thickness": "Épaisseur",
"parts.dimension.width": "Largeur",
"parts.family.bearing": "Roulement à billes à gorge profonde (enveloppe)",
"parts.family.dowel_pin": "Goupille cylindrique",
"parts.family.hex_bolt": "Vis à tête hexagonale",
"parts.family.hex_nut": "Écrou hexagonal",
"parts.family.set_screw": "Vis sans tête à six pans creux, bout plat",
"parts.family.socket_head_cap_screw": "Vis à tête cylindrique à six pans creux",
"parts.family.washer": "Rondelle plate",
"pattern.alignment_2": "Alignement 2",
"pattern.alignment_hint": "La quantité inclut l’original. Le centrage répartit les copies de chaque côté ; si la quantité est paire, l’inversion choisit le côté qui reçoit la copie supplémentaire.",
"pattern.direction_1": "Direction 1",
@@ -1390,9 +1515,25 @@
"perf.ui_time": "Temps de l'interface",
"perf.viewport": "Vue 3D",
"perf.worst": "pire",
"pick a body or some of its faces": "choisir un corps ou certaines de ses faces",
"pick a hole to seat it, or OK places it at the origin": "choisir un trou pour la loger, ou OK la place à l'origine",
"pick a planar face": "choisir une face plane",
"pick a plane or a face as the tool": "choisir un plan ou une face comme outil",
"pick a size": "choisir une taille",
"pick edges of a body": "choisir des arêtes d'un corps",
"pick edges of one body": "choisir des arêtes d'un seul corps",
"pick edges of the holes to remove": "choisir les arêtes des perçages à supprimer",
"pick faces of one body": "choisir des faces d'un seul corps",
"pick faces to offset": "choisir les faces à décaler",
"pick lines of one sketch": "choisir des lignes d'une seule esquisse",
"pick one or two vertices, edges or faces": "choisir un ou deux sommets, arêtes ou faces",
"pick one plane or face as the tool, or sketch curves": "choisir un plan ou une face comme outil, ou des courbes d'esquisse",
"pick sections in two or more sketches": "choisir des sections dans au moins deux esquisses",
"pick sketch curves or edges": "choisir des courbes d'esquisse ou des arêtes",
"pick the face to untrim": "choisir la face dont l'ajustement doit être annulé",
"pick the path": "choisir la trajectoire",
"pick what to keep": "choisir ce qu'il faut conserver",
"pick what to keep of the other surface": "choisir ce qu'il faut conserver de l'autre surface",
"prefs.add_item": "Ajouter l'élément",
"prefs.alt_frees_manipulators": "Alt / Option libère aussi les flèches du manipulateur",
"prefs.anti_aliasing_amd_dx12": "L'anticrénelage est à 1× car la vérification au démarrage n'a rien dessiné avec le multiéchantillonnage et a réussi à 1×. Vous pouvez choisir un autre réglage ci-dessus ; redémarrez pour l'appliquer si la vérification réussit.",
@@ -2156,6 +2297,26 @@
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
"search.valid": "Valide",
"section.off_hint": "Aucun plan choisi : OK désactive la coupe.",
"select a direction (an axis, a sketch line or a straight edge) first": "sélectionner d'abord une direction (un axe, une ligne d'esquisse ou une arête droite)",
"select a direction (an axis, a sketch line or a straight edge) for the profile to follow": "sélectionner une direction (un axe, une ligne d'esquisse ou une arête droite) que le profil doit suivre",
"select a point on a component": "sélectionner un point sur un composant",
"select a profile, edges or sketch lines first": "sélectionner d'abord un profil, des arêtes ou des lignes d'esquisse",
"select an axis": "sélectionner un axe",
"select an axis, a straight or circular edge, or a cylinder, cone or torus face first": "sélectionner d'abord un axe, une arête droite ou circulaire, ou une face cylindrique, conique ou torique",
"select bodies first": "sélectionner d'abord des corps",
"select corner points, or the two lines of one corner": "sélectionner des points de coin, ou les deux lignes d'un coin",
"select one corner point, or the two lines of one corner": "sélectionner un point de coin, ou les deux lignes d'un coin",
"select sheet edges first": "sélectionner d'abord des arêtes de tôle",
"select the bend line (a sketch line) first": "sélectionner d'abord la ligne de pli (une ligne d'esquisse)",
"select the cam's profile (a sketch profile or the cam's edges)": "sélectionner le profil de la came (un profil d'esquisse ou les arêtes de la came)",
"select the corner to cut first": "sélectionner d'abord le coin à chanfreiner",
"select the corners to round first": "sélectionner d'abord les coins à arrondir",
"select the fixed face first (a plate's large face)": "sélectionner d'abord la face fixe (la grande face d'une plaque)",
"select the frame sketch's lines first": "sélectionner d'abord les lignes de l'esquisse de la structure",
"select the path (sketch lines across the edge)": "sélectionner la trajectoire (lignes d'esquisse qui traversent l'arête)",
"select the rim face first": "sélectionner d'abord la face du bord",
"select the sheet edge first": "sélectionner d'abord l'arête de la tôle",
"select two components": "sélectionner deux composants",
"set by the sketch's lines": "défini par les lignes de l'esquisse",
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
"settings.frame_rate_details_note": "Ajoute un graphe des temps des dernières images avec les triangles dessinés superposés, un histogramme des temps d'image et ce que le GPU a dessiné. Cliquez sur la fréquence d'images dans la vue pour les afficher ou les masquer.",
@@ -2173,17 +2334,23 @@
"settings.window_system_details": "Détails",
"settings.window_system_note_now": "S'applique après un redémarrage. Actuellement : {now}.",
"settings.window_system_wayland_limited": "Certaines fonctionnalités sont limitées sous Wayland natif.",
"sheet.flange_type_base": "Base",
"sheet.flange_type_contour": "Contour",
"sheet.flange_type_edge": "Arête",
"sheet.no_body": "il n'y a pas de corps de tôlerie : commencez-en un avec Bord tombé > Base, ou transformez une plaque en tôlerie avec Convertir en tôlerie (choisissez sa grande face plane)",
"shortcut.json_error": "les paramètres doivent être au format JSON : {error}",
"shortcut.no_command": "la commande `{id}` n’existe pas ; le texte avant le JSON doit être un ID de commande",
"shortcut.pin": "Épingler en haut de la boîte S",
"shortcut.unpin": "Désépingler",
"sketch is missing": "l'esquisse est manquante",
"sketch.degrees_of_freedom": "{count} degrés de liberté",
"sketch.dof_left": "{count} degrés de liberté restants",
"sketch.dof_left_one": "{count} degré de liberté restant",
"sketch.draw_curves": "Dessiner en {linetype}",
"sketch.make_selection": "Convertir la sélection en {linetype}",
"sketch.open_ends": "{count} ouvertes",
"sketch.status_conflict": "contraintes en conflit",
"sketch.status_fully_constrained": "entièrement contrainte",
"sketch.untied_ends": "{count} sans coïncidence",
"sketch_panel.attached": "Attaché à {body} / {path}",
"sketch_panel.author": "Placement d’origine : {path}",
@@ -2210,7 +2377,24 @@
"sketch_text.path_hint": "Une courbe de l'esquisse que le texte suit : cliquer ici, puis sur la courbe",
"sketch_text.use_font_file": "Utiliser le fichier de police {path}",
"solid": "plein (sans paroi)",
"the angle must be a number": "l'angle doit être un nombre",
"the curves must be in one sketch": "les courbes doivent être dans une seule esquisse",
"the direction must be an axis, a sketch line or a straight edge": "la direction doit être un axe, une ligne d'esquisse ou une arête droite",
"the duration must be a number of seconds, 0.001 or more": "la durée doit être un nombre de secondes, d'au moins 0.001",
"the line's sketch is gone": "l'esquisse de la ligne n'existe plus",
"the lines must be in one sketch": "les lignes doivent être dans une seule esquisse",
"the lines must be in one sketch, not the section's": "les lignes doivent être dans une seule esquisse, pas celle de la section",
"the parts library is empty": "la bibliothèque de pièces est vide",
"the parts library is not available": "la bibliothèque de pièces n'est pas disponible",
"the path curves must be in one other sketch": "les courbes de la trajectoire doivent être dans une seule autre esquisse",
"the path's sketch is gone": "l'esquisse de la trajectoire n'existe plus",
"the position must be a number": "la position doit être un nombre",
"the preview failed": "l'aperçu a échoué",
"the profiles and curves must be in one sketch": "les profils et les courbes doivent être dans une seule esquisse",
"the reference face must be on the edges' body": "la face de référence doit appartenir au corps des arêtes",
"the scale must be above 0": "l'échelle doit être supérieure à 0",
"the section's profiles must be in one sketch": "les profils de la section doivent être dans une seule esquisse",
"the two origins are on the same component": "les deux origines se trouvent sur le même composant",
"thread.automatic_size": "Taille automatique",
"thread.designation": "Désignation",
"thread.iso_metric": "Métrique ISO",
@@ -2238,6 +2422,7 @@
"trace.points": "{count} points le long de la trajectoire",
"trace.stopped_by_contact": "Se termine à l'échantillon {sample} : {a} traverserait {b}",
"trace.stopped_by_loop": "Se termine à l'échantillon {sample} : la boucle passant par {joint} ne peut plus se fermer",
"trim by sketch curves, or by one plane or face": "ajuster avec des courbes d'esquisse, ou avec un plan ou une face",
"ui.1_degree_of_freedom": "1 degré de liberté",
"ui.3d_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
"ui.3d_mouse_button": "Bouton {number}",
@@ -2692,6 +2877,8 @@
"ui.design_properties.revision": "Indice",
"ui.diameter": "Diamètre",
"ui.dimension": "Cote",
"ui.direct_interaction": "Interaction directe",
"ui.direct_interaction_note": "Travailler directement sur le modèle dans la vue. Appuyer sur une face plane ou un profil et tirer : vers le haut extrude, vers le bas dans un corps découpe, et un mouvement latéral règle le pas",
"ui.direction": "Direction",
"ui.display_quality": "Qualité d'affichage",
"ui.display_quality_coarse": "Grossière",
@@ -2715,6 +2902,7 @@
"ui.editJointLimits": "Modifier les limites de liaison",
"ui.edit_section_analysis": "Modifier l'analyse de coupe",
"ui.ellipse": "Ellipse",
"ui.emboss_wrap_note": "Les profils de lignes orthogonales tangents à un cylindre pris en charge s'enroulent automatiquement. Choisir une paroi pour définir une cible.",
"ui.end_radius": "Rayon final",
"ui.end_scale": "Échelle finale",
"ui.entities": "Entités",
@@ -2786,6 +2974,7 @@
"ui.heading_extrude": "EXTRUSION",
"ui.heading_fillet": "CONGÉ",
"ui.heading_finish_sketch": "TERMINER L'ESQUISSE",
"ui.heading_form": "FORME",
"ui.heading_front": "AVANT",
"ui.heading_hole": "PERÇAGE",
"ui.heading_home": "ACCUEIL",
@@ -2834,6 +3023,7 @@
"ui.heading_utilities": "UTILITAIRES",
"ui.heading_utility": "UTILITAIRE",
"ui.heading_weldments": "MÉCANO-SOUDURE",
"ui.height": "Hauteur",
"ui.help": "Aide",
"ui.hide": "Masquer",
"ui.hideAxes": "Masquer les axes",
@@ -2870,11 +3060,14 @@
"ui.intersect": "Intersection",
"ui.isolate": "Isoler",
"ui.join": "Joindre",
"ui.joint_play": "▶ Lire",
"ui.joint_stop": "■ Arrêter",
"ui.joints": "Liaisons",
"ui.keyboard_shortcuts": "Raccourcis clavier",
"ui.language": "Langue",
"ui.layout_compact": "Compacte",
"ui.layout_floating": "Flottante",
"ui.length": "Longueur",
"ui.length_decimals": "Décimales des longueurs",
"ui.light": "Clair",
"ui.line": "Ligne",
@@ -2886,6 +3079,7 @@
"ui.loft": "Lissage",
"ui.look_at": "Vue normale",
"ui.look_at_anim_desc": "Vue d'axe, Vue normale, Accueil et Cadrer",
"ui.major_radius": "Grand rayon",
"ui.manipulator_free_alt": "Glissement libre du manipulateur aussi avec Alt/Option",
"ui.manipulator_free_alt_tip": "Activé : Alt/Option ou Ctrl/Cmd sur les flèches d’extrusion / du manipulateur désactive l’accrochage aux valeurs. Désactivé : Ctrl/Cmd uniquement.",
"ui.manufacture": "Fabrication",
@@ -2915,8 +3109,10 @@
"ui.mesh_format_3mf": "3MF",
"ui.mesh_format_3mf_tagged": "3MF avec étiquettes de faces",
"ui.mesh_format_stl": "STL",
"ui.minor_radius": "Petit rayon",
"ui.mirror": "Symétrie",
"ui.mirror_line": "Ligne de symétrie",
"ui.modifiers_either": "{first} ou {second}",
"ui.move": "Déplacer",
"ui.multiple": "Multiple",
"ui.named_views": "Vues nommées",
@@ -2968,6 +3164,7 @@
"ui.pinToShortcuts": "Épingler aux raccourcis",
"ui.pinToToolbar": "Épingler à la barre d'outils",
"ui.pipe": "Tuyau",
"ui.pitch": "Pas",
"ui.placement": "Placement",
"ui.plane": "Plan",
"ui.plane_at_angle": "Plan incliné",
@@ -3027,6 +3224,7 @@
"ui.reverse_horizontal_pan_direction": "Inverser le sens du panoramique horizontal",
"ui.reverse_horizontal_pan_tip": "Le défilement horizontal à deux doigts et la molette latérale font le panoramique dans l'autre sens",
"ui.reverse_zoom_direction": "Inverser le sens du zoom",
"ui.revolutions": "Tours",
"ui.revolve": "Révolution",
"ui.ribbon_exit": "Quitter",
"ui.ribbon_labels": "Afficher le texte sous les icônes du ruban",
@@ -3042,6 +3240,7 @@
"ui.scale": "Échelle",
"ui.second_rail": "Second rail",
"ui.section_analysis": "Analyse de coupe",
"ui.section_size": "Taille de la section",
"ui.select": "Sélectionner",
"ui.selectOther": "Sélectionner un autre…",
"ui.selectTangentChain": "Sélectionner la chaîne tangente",
@@ -3102,6 +3301,7 @@
"ui.start_page": "Page d’accueil",
"ui.step_values": "Incrémenter les valeurs avec les flèches et la molette",
"ui.step_values_tip": "Dans un champ de valeur en cours de saisie, Haut/Bas modifient un nombre simple de 1 (Maj : 10, Alt : 0,1), de même que la molette de la souris au-dessus du champ. Les expressions restent telles que saisies.",
"ui.stitch_tolerance_hint": "serrée",
"ui.stretch": "Étirer",
"ui.supersampling": "Suréchantillonnage (SSAA)",
"ui.supersampling_adaptive": "Seulement quand la vue est immobile",
@@ -3132,6 +3332,8 @@
"ui.to_object": "Jusqu'à l'objet",
"ui.tool_direction": "Direction de l'outil",
"ui.top": "Dessus",
"ui.top_diameter": "Diamètre supérieur",
"ui.top_radius": "Rayon supérieur",
"ui.torus": "Tore",
"ui.total_angle": "Angle total",
"ui.trackpad_pan_tip": "Les gestes du pavé tactile font un panoramique (désactivé : ils zooment comme la molette) ; la molette zoome, la molette latérale fait un panoramique horizontal, le pincement zoome toujours",
@@ -3160,6 +3362,7 @@
"ui.visual_shaded_with_edges": "Ombré avec arêtes",
"ui.visual_style": "Style visuel",
"ui.visual_wireframe": "Filaire",
"ui.width": "Largeur",
"ui.window_system": "Système de fenêtrage",
"ui.window_system_auto": "Automatique",
"ui.window_system_wayland": "Wayland",
@@ -3220,6 +3423,9 @@
"update.up_to_date": "SolidCraft est à jour.",
"update.update_now": "Mettre à jour maintenant",
"update.you_have": "Vous avez la version {version}.",
"use a positive nominal diameter and a pitch smaller than it": "utiliser un diamètre nominal positif et un pas inférieur à ce diamètre",
"values must be finite numbers": "les valeurs doivent être des nombres finis",
"values must be numbers or expressions": "les valeurs doivent être des nombres ou des expressions",
"web.files.insert_drawings_hint": "Fichiers CAO, maillages, DXF ou SVG dans la conception ouverte",
"web.files.insert_stored_drawings_hint": "Dans la conception ouverte (fichiers CAO, maillages, DXF, SVG)",
"web.open_full": "Ouvrir dans SolidCraft",
+1605 -1
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1663 -1
View File
File diff suppressed because it is too large Load Diff
+1615 -1
View File
File diff suppressed because it is too large Load Diff
+1630 -1
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1605 -1
View File
File diff suppressed because it is too large Load Diff
+1415 -1
View File
File diff suppressed because it is too large Load Diff
+1661 -1
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -26,6 +26,7 @@
"All open designs": "Wszystkie otwarte projekty",
"Allowed expressions, separated by semicolons": "Dozwolone wyrażenia, rozdzielone średnikami",
"Also deletes": "Usuwa również",
"Also place them from the origin (fully constrained)": "Umieść je też względem początku (w pełni związane)",
"Angle 2": "Kąt 2",
"Animate": "Animuj",
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
@@ -118,6 +119,7 @@
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
"Dimension new shapes": "Wymiaruj nowe kształty",
"Direction": "Kierunek",
"Discard": "Odrzuć",
"Do not export hidden edges": "Nie eksportuj krawędzi niewidocznych",
@@ -131,6 +133,8 @@
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
"Drive Joint": "Steruj złączem",
"Duration": "Czas trwania",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Każdy nowy kształt otrzymuje też wymiary od początku szkicu, aż nic nie pozostanie swobodne. Kształt już połączony z początkiem lub z umieszczoną geometrią nie otrzymuje żadnych.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Każdy narysowany kształt otrzymuje wszystkie swoje wymiary w narysowanych rozmiarach (wpisane wartości mają pierwszeństwo). Rozmiar, który szkic już określa, jest pomijany.",
"Edges": "Krawędzie",
"Edit": "Edytuj",
"Edit Profile Sketch": "Edytuj szkic profilu",
@@ -1057,6 +1061,11 @@
"no picture": "brak obrazu",
"none": "brak",
"parts.automatic_name": "automatycznie",
"parts.dimension.d": "Średnica nominalna",
"parts.dimension.height": "Wysokość",
"parts.dimension.pitch": "Skok",
"parts.dimension.thickness": "Grubość",
"parts.dimension.width": "Szerokość",
"pattern.alignment_2": "Wyrównanie 2",
"pattern.alignment_hint": "Liczba obejmuje oryginał. Wyśrodkuj rozkłada kopie po obu stronach; Odwróć wybiera stronę z dodatkową kopią przy parzystej liczbie.",
"pattern.direction_1": "Kierunek 1",
@@ -1893,6 +1902,7 @@
"ui.heading_utilities": "NARZĘDZIA",
"ui.heading_utility": "NARZĘDZIE",
"ui.heading_weldments": "SPAWANIE",
"ui.height": "Wysokość",
"ui.help": "Pomoc",
"ui.hide": "Ukryj",
"ui.hideAxes": "Ukryj osie",
@@ -1931,6 +1941,7 @@
"ui.language": "Język",
"ui.layout_compact": "Kompaktowy",
"ui.layout_floating": "Pływający",
"ui.length": "Długość",
"ui.length_decimals": "Miejsca dziesiętne długości",
"ui.light": "Jasny",
"ui.line": "Linia",
@@ -2019,6 +2030,7 @@
"ui.pinToShortcuts": "Przypnij do skrótów",
"ui.pinToToolbar": "Przypnij do paska narzędzi",
"ui.pipe": "Rura",
"ui.pitch": "Skok",
"ui.placement": "Umiejscowienie",
"ui.placement_handles": "Uchwyty umieszczania",
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
@@ -2194,6 +2206,7 @@
"ui.visual_shaded_with_edges": "Cieniowany z krawędziami",
"ui.visual_style": "Styl wizualny",
"ui.visual_wireframe": "Model krawędziowy",
"ui.width": "Szerokość",
"ui.workspace": "środowisko pracy",
"ui.workspace_animation_description": "Widoki rozstrzelone, badania ruchu i scenorysy",
"ui.workspace_available": "Dostępne",
+15
View File
@@ -30,6 +30,7 @@
"All open designs": "Todos os projetos abertos",
"Allowed expressions, separated by semicolons": "Expressões permitidas, separadas por ponto e vírgula",
"Also deletes": "Também exclui",
"Also place them from the origin (fully constrained)": "Posicioná-las também a partir da origem (totalmente restritas)",
"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.",
@@ -145,6 +146,7 @@
"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",
"Dimension new shapes": "Cotar novas formas",
"Direction": "Direção",
"Discard": "Descartar",
"Distance": "Distância",
@@ -155,6 +157,8 @@
"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": "Clique duplo com o botão do meio na vista: Início (a vista inicial, enquadrando o que é mostrado), Ajustar (enquadra o que é mostrado, mantendo a orientação) ou nada",
"Drive Joint": "Acionar junta",
"Duration": "Duração",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada nova forma também recebe cotas a partir da origem do esboço até nada ficar livre. Uma forma já ligada à origem ou a uma geometria posicionada não recebe nenhuma.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que você desenha recebe todas as suas cotas nos tamanhos desenhados (valores digitados têm prioridade). Um tamanho que o esboço já define é omitido.",
"Edges": "Arestas",
"Edit": "Editar",
"Edit Profile Sketch": "Editar esboço do perfil",
@@ -1159,7 +1163,9 @@
"nav.free_orbit.label": "Órbita livre",
"nav.free_orbit.snap_help": "Mouse e mouse 3D giram através dos dois polos e preservam a rotação da imagem. Mantenha Shift pressionado durante uma órbita por arraste para alinhar a direção da vista e a rotação da imagem.",
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.hfss.help": "Alt+arrastar com o esquerdo desloca, Shift+Alt+esquerdo aplica zoom, Ctrl+arrastar com o esquerdo em área vazia orbita",
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.maya.help": "Alt+arrastar com o esquerdo orbita, Alt+meio desloca, Alt+direito aplica zoom; arrastar com o meio também desloca",
"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",
@@ -1178,6 +1184,11 @@
"no picture": "sem imagem",
"none": "nenhum",
"parts.automatic_name": "automático",
"parts.dimension.d": "Diâmetro nominal",
"parts.dimension.height": "Altura",
"parts.dimension.pitch": "Passo",
"parts.dimension.thickness": "Espessura",
"parts.dimension.width": "Largura",
"pattern.alignment_2": "Alinhamento 2",
"pattern.alignment_hint": "As contagens incluem o original. Centralizar equilibra as cópias; Inverter escolhe o lado extra para totais pares.",
"pattern.direction_1": "Direção 1",
@@ -2023,6 +2034,7 @@
"ui.heading_utilities": "UTILITÁRIOS",
"ui.heading_utility": "UTILITÁRIO",
"ui.heading_weldments": "SOLDAGEM",
"ui.height": "Altura",
"ui.help": "Ajuda",
"ui.hide": "Ocultar",
"ui.hideAxes": "Ocultar eixos",
@@ -2061,6 +2073,7 @@
"ui.language": "Idioma",
"ui.layout_compact": "Compacto",
"ui.layout_floating": "Flutuante",
"ui.length": "Comprimento",
"ui.length_decimals": "Casas decimais de comprimento",
"ui.light": "Claro",
"ui.line": "Linha",
@@ -2148,6 +2161,7 @@
"ui.pinToShortcuts": "Fixar nos atalhos",
"ui.pinToToolbar": "Fixar na barra de ferramentas",
"ui.pipe": "Tubo",
"ui.pitch": "Passo",
"ui.placement": "Posicionamento",
"ui.plane": "Plano",
"ui.plane_at_angle": "Plano em ângulo",
@@ -2322,6 +2336,7 @@
"ui.visual_shaded_with_edges": "Sombreado com arestas",
"ui.visual_style": "Estilo visual",
"ui.visual_wireframe": "Aramado",
"ui.width": "Largura",
"ui.workspace": "espaço de trabalho",
"ui.workspace_animation_description": "Vistas explodidas, estudos de movimento e storyboards",
"ui.workspace_available": "Disponível",
+957 -1
View File
File diff suppressed because it is too large Load Diff
+852 -1
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+957 -1
View File
File diff suppressed because it is too large Load Diff
+216 -1
View File
@@ -22,10 +22,12 @@
"Add Mid Radius": "Ara yarıçap ekle",
"Add Row": "Satır ekle",
"Add a tool": "Araç ekle",
"Add all bodies": "Tüm gövdeleri ekle",
"Add bend": "Büküm ekle",
"Add condition": "Koşul ekle",
"Add or edit rule": "Kural ekle veya düzenle",
"Add to New Folder": "Yeni Klasöre Ekle",
"Add visible bodies": "Görünür gövdeleri ekle",
"Advanced": "Gelişmiş",
"Against": "Karşı",
"Alignment": "Hizalama",
@@ -35,6 +37,7 @@
"All open designs": "Tüm açık tasarımlar",
"Allowed expressions, separated by semicolons": "Noktalı virgülle ayrılmış izin verilen ifadeler",
"Also deletes": "Bunları da siler",
"Also place them from the origin (fully constrained)": "Orijinden de konumlandır (tam kısıtlı)",
"Anchor": "Çapa",
"Angle 2": "Açı 2",
"Angle Display": "Açı görünümü",
@@ -56,6 +59,7 @@
"BOUNDARY SURFACE": "SINIR YÜZEYİ",
"BROWSER": "TARAYICI",
"Bar": "Çubuk",
"Base Center": "Taban Merkezi",
"Bend": "Büküm",
"Bend Edges": "Büküm kenarları",
"Bend Line Position": "Büküm çizgisi konumu",
@@ -140,6 +144,7 @@
"Create Issue — review draft": "Sorun oluştur — taslağı gözden geçirin",
"Create Issue…": "Sorun Oluştur…",
"Cross curves": "Çapraz eğriler",
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter ile GitHub'da devam edilir.",
"Cube": "Küp",
"Current orthographic view (1:1 mm)": "Geçerli ortografik görünüm (1:1 mm)",
"Current perspective view": "Geçerli perspektif görünüm",
@@ -152,6 +157,7 @@
"Customize…": "Özelleştir…",
"Cylindrical face": "Silindirik yüz",
"Delete": "Sil",
"Delete Folders and Features…": "Klasörleri ve Özellikleri Sil…",
"Delete Selected Features…": "Seçili Özellikleri Sil…",
"Delete Selected Folders": "Seçili Klasörleri Sil",
"Delete all": "Tümünü sil",
@@ -165,6 +171,7 @@
"Designs you open or save appear here.": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Nesneye kadar seçeneğiyle belirlenir. Açısal kapsamı düzenlemek için Açı seçeneğini seçin.",
"Diameter": "Çap",
"Dimension new shapes": "Yeni şekillere ölçü ver",
"Direction": "Yön",
"Discard": "At",
"Distance": "Mesafe",
@@ -181,6 +188,8 @@
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Dayamayı 5° adımlarla çevirmek için sürükleyin. Serbestçe çevirmek için Alt veya Ctrl tuşunu basılı tutun.",
"Drive Joint": "Bağlantıyı tahrik et",
"Duration": "Süre",
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Her yeni şekle, serbest bir şey kalmayana kadar çizim orijininden de ölçü verilir. Orijine veya yerleştirilmiş geometriye zaten bağlı bir şekle verilmez.",
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Çizdiğiniz her şekil, çizdiğiniz boyutlarda tüm ölçülerini alır (yazılan değerler önceliklidir). Çizimin zaten belirlediği bir boyut atlanır.",
"Edges": "Kenarlar",
"Edit": "Düzenle",
"Edit Profile Sketch": "Profil eskizini düzenle",
@@ -430,6 +439,7 @@
"Remove this point": "Bu noktayı kaldır",
"Rename Feature": "Özelliği Yeniden Adlandır",
"Rename Folder": "Klasörü Yeniden Adlandır",
"Render Stage": "Render Platosu",
"Reopen designs": "Tasarımları yeniden aç",
"Reopen the designs that were open when SolidCraft was last closed": "SolidCraft son kapatıldığında açık olan tasarımları yeniden aç",
"Replace existing library file": "Mevcut kitaplık dosyasının üzerine yaz",
@@ -534,6 +544,7 @@
"Style": "Stil",
"Summary": "Özet",
"Suppress": "Bastır",
"Suppress Selected Folders": "Seçili Klasörleri Bastır",
"Suppressed": "Bastırılmış",
"Surface": "Yüzey",
"Surface, or faces to turn over": "Yüzey veya çevrilecek yüzler",
@@ -581,6 +592,7 @@
"UNTRIM": "KIRPMAYI KALDIR",
"Unit": "Birim",
"Unsuppress": "Bastırmayı kaldır",
"Unsuppress Selected Folders": "Seçili Klasörlerin Bastırmasını Kaldır",
"Updating the preview": "Önizleme güncelleniyor",
"Use active author placement": "Etkin kaynak yerleşimini kullan",
"Use fixed plane": "Sabit düzlem kullan",
@@ -628,6 +640,8 @@
"ai.prefs.background_commands": "Ajan komutlarını arka planda çalıştır",
"ai.prefs.connect_apps": "SolidCraft'ı bu bilgisayardaki AI uygulamalarına aç",
"ai.prefs.connect_apps_note": "SolidCraft başladığında, bu bilgisayarda bulduğu AI uygulamalarına (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code) onu bir MCP sunucusu olarak ekler; böylece bir ajan kurulum yapmadan kullanabilir. Yalnızca solidcraft girdisi eklenir veya yolu yenilenir; değiştirdiğiniz bir girdiye dokunulmaz ve dosya önce yedeklenir. Kapalı: eklediği girdiler kaldırılır. Yardım > AI asistanlarını bağla… bunu elle yapar.",
"ai.prefs.connect_at_start": "SolidCraft başladığında en son kullanılan ajana bağlan",
"ai.prefs.connect_at_start_note": "SolidCraft her başladığında, en son bağlandığınız ajanı (veya varsayılanı) arka planda başlatır; böylece sohbete hazır olur. SolidCraft --control ile başlatıldığında çalışmaz. Bağlanamazsa Asistan, Bağlantı başarısız mesajını ve bir Yeniden dene düğmesini gösterir.",
"ai.prefs.follow_camera": "Kamerayı eskizlere çevir",
"ai.prefs.follow_camera_off": "Otomatik kamera takibi kapalı.",
"ai.prefs.follow_camera_on": "Kamera, ajanın oluşturduğu eskizleri görecek şekilde döner.",
@@ -635,6 +649,8 @@
"ai.prefs.make_default": "Varsayılan olarak kullan",
"ai.prefs.no_agents": "Henüz ajan yok. Ajan ekleyin veya yüklü ajanları bulmak için yenileyin.",
"ai.prefs.no_saved_approvals": "Kaydedilmiş onay yok.",
"ai.prefs.open_at_start": "SolidCraft başladığında Asistan panelini aç",
"ai.prefs.open_at_start_note": "SolidCraft her başladığında Asistan panelini (veya kendi penceresini) gösterir. SolidCraft --control ile başlatıldığında gösterilmez.",
"ai.prefs.override_all_warning": "Bu ajan, sormadan tasarımı kaydedebilir, üzerine yazabilir, dışa aktarabilir veya değiştirebilir.",
"ai.prefs.permission_all": "Her şeye izin ver",
"ai.prefs.permission_all_summary": "Kullanılabilir SolidCraft araçları sormadan çalışır.",
@@ -679,6 +695,9 @@
"cmd.appearance_assign": "Görünüm",
"cmd.appearance_library": "Görünüm kitaplığı",
"cmd.appearance_list": "Görünümleri listele",
"cmd.appearance_paint": "Görünümü Boya",
"cmd.appearance_pick_up": "Görünümü Al",
"cmd.appearance_replace": "Görünümü Değiştir",
"cmd.assembly_bom": "Malzeme listesi",
"cmd.body_rename": "Gövdeyi yeniden adlandır",
"cmd.browser_collapse_all": "Tümünü Daralt",
@@ -766,6 +785,7 @@
"cmd.document_revision": "Tasarım Revizyonu",
"cmd.document_units": "Tasarımın birimlerini değiştir",
"cmd.edit_checkpoint": "Kontrol noktası",
"cmd.edit_repeat": "Son Komutu Tekrarla",
"cmd.edit_restore_checkpoint": "Kontrol noktasını geri yükle",
"cmd.engine_commands": "Komutları listele",
"cmd.engine_describe": "Komutu açıkla",
@@ -879,9 +899,11 @@
"cmd.plastic_snap_fit": "Geçmeli bağlantı",
"cmd.render_aging_describe": "Katmanlı Yüzey Yaşlanması",
"cmd.render_backends": "Render arka uçları",
"cmd.render_batch": "Tüm Çekimleri Render Et",
"cmd.render_camera_fit": "Render Konusunu Çerçevele",
"cmd.render_camera_get": "Render Kamerasını İncele",
"cmd.render_cancel": "Render'ı iptal et",
"cmd.render_compare": "Görüntüleri Karşılaştır",
"cmd.render_effects_describe": "Mercek ve Yaratıcı Efektler",
"cmd.render_export": "PNG dışa aktar",
"cmd.render_hdr_inspect": "HDR Kaynağını İncele",
@@ -897,6 +919,7 @@
"cmd.render_library_surface_prepare": "Oluşturulmuş Yüzey Haritalarını Hazırla",
"cmd.render_library_surface_unbind": "Yüzey Bağlamını Kaldır",
"cmd.render_library_targets": "Render Kitaplık Hedeflerini Bul",
"cmd.render_lights_solo": "Işığı Solo Yap",
"cmd.render_optics_bind": "Optik Bağlamı Kaydet",
"cmd.render_optics_unbind": "Optik Bağlamı Kaldır",
"cmd.render_preflight": "Render isteğini kontrol et",
@@ -1006,7 +1029,9 @@
"cmd.sketch_spline_control_point": "Kontrol noktalı spline",
"cmd.sketch_spline_control_point_5": "Kontrol noktalı spline (derece 5)",
"cmd.sketch_spline_fit_point": "Uydurma noktalı spline",
"cmd.sketch_spline_insert_point": "Spline Noktası Ekle",
"cmd.sketch_spun_profile": "Döndürülmüş profil",
"cmd.sketch_toggle_diameter": "Yarıçap / Çap Arasında Geçiş Yap",
"cmd.sketch_toggle_driven": "Türetilmiş ölçüyü aç/kapat",
"cmd.sketch_transform": "Dönüşüm",
"cmd.sketch_trim_path": "Yol boyunca buda",
@@ -1072,6 +1097,7 @@
"cmd.view_rename": "Adlandırılmış görünümü yeniden adlandır",
"cmd.view_right": "Sağ Görünüm",
"cmd.view_save": "Yeni adlandırılmış görünüm",
"cmd.view_shots_strip": "Çekim Şeridi",
"cmd.view_spin": "Sürekli döndürme",
"cmd.view_tool": "Gezinme aracı",
"cmd.view_top": "Üst Görünüm",
@@ -1145,6 +1171,7 @@
"document.save_changes": "{name} içindeki değişiklikler kaydedilsin mi?",
"document.save_copy": "Bir Kopya Kaydet…",
"document.unsaved_changes": "{name} kaydedilmemiş değişiklikler içeriyor. Kaydetmezseniz bunlar kaybolur.",
"drive.rigid": "{joint} rijit bir bağlantıdır: parçalarını sabit tutar, bu yüzden tahrik edilecek bir şey yoktur. Hareket etmelerini sağlamak için türünü Bağlantıyı düzenle içinde değiştirin: dönmek için Döner, kaymak için Kayar, ikisi için Silindirik.",
"drive.stopped_by_contact": "{value} değerinde durdu: {a} ile {b} iç içe geçerdi",
"drive.stopped_by_loop": "{value} değerinde durdu: {joint} üzerinden geçen döngü daha fazla kapatılamıyor",
"drive.unconnected": "Hiçbir şey hareket etmiyor: {part} sabitlenmiş hiçbir parçaya bağlı değil",
@@ -1180,28 +1207,44 @@
"from the profile": "profilden",
"from the rule": "kuraldan",
"help.apache": "Apache lisansı 2.0",
"help.appearance.assign": "Seçili gövdelere, yüzlere veya bileşenlere ağırlıklarını değiştirmeden bir renk veya malzeme görünümü verir. Parçaları ayırt etmek veya bir kaplamayı önizlemek için kullanın.",
"help.attributions": "Atıflar (ATTRIBUTION.md)",
"help.build": "Sürüm {version} · commit {commit} · derleme tarihi {date}",
"help.connect_note": "SolidCraft sunucusunu yüklemek için her asistanı yeniden başlatın. Değişmeden önce her dosya .bak olarak yedeklendi.",
"help.connect_title": "AI asistanlarını bağla",
"help.face.delete": "Seçili yüzleri kaldırır ve komşu yüzlerin boşluğu kapatmasını sağlar ya da boşluğu yüzey olarak açık bırakır. Yuvarlatılmış bir kenarı, bir deliği veya yolda duran bir çıkıntıyı gidermek için kullanın.",
"help.font_details": "Arayüz, egui'nin yerleşik yazı tiplerini kullanır (Ubuntu Font Licence ve MIT/Bitstream Vera lisansları altında Ubuntu Light ve Hack; OFL ve MIT altında Noto Emoji ve emoji simge yazı tipi). Eskiz metni Liberation Sans (SIL OFL 1.1) kullanır. The Crafting Apps'in isteğe bağlı yazı tipi paketi craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), SolidCraft derlemelerine dahil değildir.",
"help.fonts": "Yazı tipleri",
"help.licences": "Lisanslar",
"help.material.assign": "Seçili gövdelerin neyden yapıldığını belirler: çelik, alüminyum veya ABS gibi. Kütle ve ağırlığın gerçek parçayla uyuşması için kullanın.",
"help.mit": "MIT lisansı",
"help.notice": "NOTICE",
"help.parameters.change": "Tasarımın ölçülerini yöneten adlandırılmış değerlerin ve ifadelerin tablosunu açar. Bir ölçüyü tek yerden değiştirip onu kullanan her özelliğin buna uymasını sağlamak için kullanın.",
"help.solid.align": "Seçili gövdeleri, üzerlerindeki bir nokta, yüz veya düzlem başka bir yerdeki nokta, yüz veya düzlemle buluşacak şekilde taşır. Ofseti ölçmeden bir parçayı diğerinin yanına yerleştirmek için kullanın.",
"help.solid.chamfer": "Seçili kenarları, belirlediğiniz mesafede düz bir pahla keser. Keskin kenarları kırmak veya bir pim ya da vidanın girişini kolaylaştırmak için kullanın.",
"help.solid.combine": "Gövdeleri birleştirir, bir hedef gövdeden çıkarır veya yalnızca paylaştıkları hacmi tutar ve araç gövdelerini tutabilir. Parçaları kaynaştırmak, bir şekli diğeriyle kesmek veya iki gövdenin örtüştüğü yeri bulmak için kullanın.",
"help.solid.draft": "Seçili yüzleri, nötr bir düzlemden ölçülen belirlediğiniz açı kadar eğerek koniklik verir. Kalıplanan veya dökülen parçaların kalıptan kolayca çıkması için kullanın.",
"help.solid.extrude": "Bir eskiz profilini, malzeme ekleyerek veya kaldırarak bir mesafe boyunca 3B'ye taşır. Düz bir konturu blok, kabarıklık veya cep haline getirmek için kullanın.",
"help.solid.fillet": "Seçili kenarları, belirlediğiniz yarıçapta pürüzsüz dairesel bir uyumla yuvarlatır. Keskin kenarları sağlamlık, konfor veya görünüm için yumuşatmak amacıyla kullanın.",
"help.solid.hole": "Bir yüzeyde seçtiğiniz noktada, belirlediğiniz çapta yuvarlak bir delik deler. Vida boşlukları, pimleri ve diğer basit yuvarlak açıklıklar için kullanın.",
"help.solid.loft": "İki veya daha fazla eskiz profili arasında, ya da bir profille bir nokta arasında pürüzsüz bir katı karıştırır. Geçişler, huniler, saplar ve uzunluğu boyunca kesiti değişen şekiller için kullanın.",
"help.solid.mirror": "Seçili gövdelerin veya özelliklerin bir düzlem boyunca ayna görüntüsünü kopyasını oluşturur ve orijinalle birleştirebilir. Simetrik parçalar için kullanın; yarısını modelleyip ikisini de alın.",
"help.solid.move": "Seçili gövdeleri ya da kopyalarını, belirlediğiniz mesafe ve açıyla taşır veya döndürür. Bir gövdenin yerini değiştirmek ya da başka bir yerde ikincisini oluşturmak için kullanın.",
"help.solid.offset_face": "Seçili yüzleri belirlediğiniz mesafe kadar içeri veya dışarı iter, çevrelerindeki yüzler de buna uyar. Eskizi düzenlemeden bir duvarı kalınlaştırmak, bir cebi derinleştirmek veya bir çıkıntıyı yükseltmek için kullanın.",
"help.solid.pattern.circular": "Seçili nesneleri, belirlediğiniz adet ve toplam açı boyunca bir eksen etrafında yineler. Cıvata daireleri, jant kolları, kanatçıklar ve dişler için kullanın.",
"help.solid.pattern.rectangular": "Seçili nesneleri, belirlediğiniz adet ve aralıkla bir veya iki yönde yineler. Delik, nervür, pim veya ayak dizileri ve ızgaraları için kullanın.",
"help.solid.press_pull": "Bir yüzü sürüklerken malzeme ekler veya kaldırır, kenarları sürüklerken yuvarlatır. Doğrudan model üzerinde hızlı değişiklikler için kullanın.",
"help.solid.remove": "Seçili gövdeleri tasarımdan, zaman çizelgesindeki bu noktadan itibaren çıkarır; önceki adımlar onları korur. Artık gerekmeyen bir kesici araç gibi yardımcı bir gövdeyi atmak için kullanın.",
"help.solid.replace_face": "Seçili düz yüzleri bir düzleme veya paralel bir yüze taşır, gövde de onunla buluşmak için büyür veya küçülür. Bir yüzü başka bir parçayla ya da bir yapı düzlemiyle aynı hizaya getirmek için kullanın.",
"help.solid.revolve": "Bir eskiz profilini bir eksen etrafında döndürerek yuvarlak bir katı oluşturur. Miller, topuzlar, kaseler ve bir merkez çizgisi etrafında yuvarlak olan parçalar için kullanın.",
"help.solid.scale": "Seçili gövdeleri bir çarpanla büyütür veya küçültür; her yönde aynı ya da eksen başına ayrı olabilir. İçe aktarılan bir şekli yeniden boyutlandırmak veya büzülme payı bırakmak için kullanın.",
"help.solid.shell": "Bir katıyı içini oyarak boşaltır, belirlediğiniz kalınlıkta duvarlar bırakır ve seçtiğiniz yüzleri açabilir. Kutular, kapaklar ve diğer ince cidarlı parçalar için kullanın.",
"help.solid.split_body": "Bir gövdeyi bir düzlemle, başka bir gövdenin yüzüyle veya bir eskiz çizgisiyle ayrı gövdelere keser. Ayrı ayrı basılan, işlenen veya renklendirilen parçalar elde etmek için kullanın.",
"help.solid.split_face": "Seçili yüzleri, gövdeyi kesmeden bir düzlem, başka bir gövdenin yüzü veya eskiz eğrileri boyunca böler. Bir yüzün bir kısmına kendi koniklik, renk veya ofsetini vermek için kullanın.",
"help.solid.sweep": "Bir eskiz profilini bir yol boyunca sürükleyerek katı oluşturur. Borular, raylar, saplar ve bir eğriyi izleyen sabit kesitli parçalar için kullanın.",
"help.third_party": "Üçüncü taraf lisansları",
"help.timeline.compute_all": "Zaman çizelgesindeki her özelliği baştan yeniden oluşturur. Modeli tazelemek ve hangi özelliklerin başarısız olduğunu görmek için kullanın.",
"help.timeline.delete": "Seçili özellikleri zaman çizelgesinden siler; bir eskizi silmek ondan oluşturulan özellikleri de siler. Artık istenmeyen bir adımı kaldırmak için kullanın.",
"hole_presets.confirm_delete": "Silmeyi onayla",
"hole_presets.delete": "Sil",
"hole_presets.delete_prompt": "Bu kişisel ön ayar silinsin mi? Mevcut delikler değişmez.",
@@ -1286,6 +1329,9 @@
"install.steps_intro": "Safari web uygulamalarını kendi menüleri üzerinden yükler:",
"install.title": "SolidCraft'ı yükle",
"issue.body_label": "Rapor metni",
"issue.note_changed_while_waiting": "Önizleme beklenirken ayarlar veya model değişti. Teknik ayrıntıları geçerli görünüme göre güncellemek için ekran görüntüsünü yeniden alın.",
"issue.note_details_kept": "Ekran görüntüsü yerel olarak alındı. Düzenlediğiniz teknik ayrıntılar korundu: hâlâ geçerli görünümle uyuştuklarını kontrol edin.",
"issue.note_details_updated": "Teknik ayrıntılar geçerli görünümle uyuşacak şekilde güncellendi. Ekran görüntüsü yerel olarak alındı. GitHub'a eklemeden önce gözden geçirin.",
"issue.note_image_copied": "Görseli GitHub taslağına yapıştırın; görsel kopyalama kullanılamıyorsa PNG'yi kaydedin.",
"issue.note_opened": "GitHub taslağını gözden geçirin ve ekran görüntüsünü ekleyin. GitHub'daki Submit new issue düğmesi raporu yayımlar.",
"issue.note_opened_copied": "Rapor kopyalandı. GitHub taslağına yapıştırın, ekran görüntüsünü ekleyin, ardından GitHub'da Submit new issue düğmesine tıklayın.",
@@ -1352,6 +1398,8 @@
"material.editor.unknown_card": "Mühendislik özellikleri bilinmiyor. Kaydetme yalnızca atanan yoğunluğu ve mevcut görünümü koruyabilir.",
"material.editor.user_supplied": "Kullanıcı tarafından sağlanan",
"material.editor.wood": "Ahşap",
"material.selection.choose": "Bir malzeme seçin",
"material.selection.mixed": "Farklı malzemeler",
"measure.at": "Konum",
"measure.item": "Öğe {number}",
"messages.ago_h": "{n} sa. önce",
@@ -1381,8 +1429,12 @@
"nav.free_orbit.help": "Fare ve 3B fare, her iki kutuptan geçerek döndürür ve yuvarlanmayı korur. Shift eksen hizalama devre dışı.",
"nav.free_orbit.label": "Serbest döndürme",
"nav.free_orbit.snap_help": "Fare ve 3B fare, her iki kutuptan geçerek döndürür ve yuvarlanmayı korur. Görünüm yönünü ve yuvarlanmayı hizalamak için döndürme sürüklemesi sırasında Shift tuşunu basılı tutun.",
"nav.free_orbit_upright.help": "Fareyle yana sürüklemek modeli dünyanın dikey ekseni etrafında döndürür; yukarı ve aşağı sürüklemek tepe noktasını da aşarak sınırsız eğim verir.",
"nav.free_orbit_upright.label": "Dikey eksen etrafında döndür",
"nav.fusion.help": "Orta sürükleme kaydırır, Shift+orta döndürür, sağ sürükleme döndürür",
"nav.hfss.help": "Alt+sol sürükleme kaydırır, Shift+Alt+sol yakınlaştırır, boş alanda Ctrl+sol sürükleme döndürür",
"nav.inventor.help": "Orta sürükleme kaydırır, Shift+orta döndürür, Ctrl+orta yakınlaştırır, sağ sürükleme döndürür",
"nav.maya.help": "Alt+sol sürükleme döndürür, Alt+orta kaydırır, Alt+sağ yakınlaştırır; orta sürükleme ayrıca kaydırır",
"nav.openscad.help": "Boşlukta sol sürükleme döndürür, Shift+sol sürükleme kutu seçimi yapar, eskiz geometrisi sürükleyerek düzenlenir; sağ veya orta sürükleme kaydırır, Shift+sağ veya Shift+orta sürükleme yakınlaştırır",
"nav.perspective.sketch": "Eskiz düzenleme dik görünüm kullanır. Perspektife geçmek için eskizi bitirin.",
"nav.solidworks.help": "Orta sürükleme döndürür, Ctrl+orta kaydırır, Shift+orta yakınlaştırır, sağ sürükleme döndürür",
@@ -1393,6 +1445,93 @@
"nav.touchpad.pan": "Kaydır (Shift tuşunu basılı tutup hareket ettirin; fare tuşu olmadan)",
"nav.touchpad.scroll": "Dokunmatik yüzey yakınlaştırmak için dikey kaydırmayı kullanır. İki parmakla kaydırma diğer ön ayarlar için kayıtlı kalır.",
"nav.touchpad.zoom": "Yakınlaştır (dikey kaydırın veya parmaklarınızı açıp kapatın)",
"navbar.agent_chat": "Asistan sohbet paneli",
"navbar.agent_cursor": "Ajan imlecini göster",
"navbar.ambient_occlusion": "Ortam oklüzyonu",
"navbar.assistant": "Asistan",
"navbar.custom.add_slot": "{slot} ekle",
"navbar.custom.always": "Her zaman görünür",
"navbar.custom.available": "{available} · çubuğa sürükleyin veya Ekle'ye basın",
"navbar.custom.buttons": "Düğmeler",
"navbar.custom.compact": "Kompakt",
"navbar.custom.count": "16 yuvadan {count} tanesi",
"navbar.custom.customise": "Gezinme çubuğunu özelleştir…",
"navbar.custom.design": "Tasarım",
"navbar.custom.duplicate": "Bu yuva zaten çubukta.",
"navbar.custom.fade": "Soldur",
"navbar.custom.full": "Başka bir yuva eklemek için bir yuvayı kaldırın.",
"navbar.custom.hidden_design": "Tasarımda gizli",
"navbar.custom.hidden_render": "Render'da gizli",
"navbar.custom.hide": "Gezinme çubuğunu gizle",
"navbar.custom.hide_idle": "Üzerine gelinceye kadar gizle",
"navbar.custom.icon_only": "Yalnızca simge",
"navbar.custom.icon_value": "Simge ve değer",
"navbar.custom.icons": "Simgeler",
"navbar.custom.idle": "Boştayken",
"navbar.custom.intro": "3B görünümün altındaki çubukta hangi denetimlerin ve hangi sırayla olacağını seçin. Bir yuvayı taşımak için sürükleyin, kaldırmak için çubuğun dışına sürükleyin.",
"navbar.custom.labels": "Simgeler ve etiketler",
"navbar.custom.left": "Sola taşı",
"navbar.custom.position": "Konum",
"navbar.custom.position_centre": "Orta",
"navbar.custom.position_left": "Sol",
"navbar.custom.position_right": "Sağ",
"navbar.custom.preview_as": "Şu şekilde önizle",
"navbar.custom.regular": "Normal",
"navbar.custom.remove": "Kaldır",
"navbar.custom.remove_bar": "Çubuktan kaldır",
"navbar.custom.render": "Render",
"navbar.custom.reset": "Gezinme çubuğunu sıfırla",
"navbar.custom.reset_default": "Varsayılana sıfırla",
"navbar.custom.right": "Sağa taşı",
"navbar.custom.select": "Yuva seçilmedi. Taşımak, kaldırmak veya görünümünü değiştirmek için çubukta bir yuva seçin.",
"navbar.custom.separators_full": "Çubukta zaten dört ayırıcı var.",
"navbar.custom.show": "Gezinme çubuğunu göster",
"navbar.custom.shows": "Gösterim",
"navbar.custom.size": "Boyut",
"navbar.custom.stale": "Çubuk değişti. Yuvayı yeniden seçin.",
"navbar.custom.title": "Gezinme çubuğu",
"navbar.custom.unknown": "Bu yuva kullanılamıyor.",
"navbar.description.ambient_occlusion": "Derinlik katmak için kıvrımları ve köşeleri koyulaştırır.",
"navbar.description.display": "Görünüm ayarlarını barındırır: grafik hazır ayarı, ızgara, zemin gölgesi, ortam oklüzyonu ve projeksiyon.",
"navbar.description.fit": "Seçimi çerçeveler; hiçbir şey seçili değilse tüm modeli.",
"navbar.description.grid": "Zemin ızgarasını gösterir veya gizler.",
"navbar.description.ground_shadow": "Modelin altındaki gölgeyi gösterir veya gizler.",
"navbar.description.home": "Tüm modelin izometrik görünümüne döner.",
"navbar.description.look_at": "Görünümü seçili yüze, düzleme veya açık eskize doğru çevirir.",
"navbar.description.orbit": "Görünümü modelin etrafında döndürür.",
"navbar.description.pan": "Görünümü yana, yukarı veya aşağı kaydırır.",
"navbar.description.projection": "Ortografik ve perspektif görünümler arasında geçiş yapar.",
"navbar.description.separator": "Komşu yuvaları gruplayan ince bir çizgi.",
"navbar.description.spin": "Siz tıklayana veya bir tuşa basana kadar görünümü döndürmeye devam eder.",
"navbar.description.visual_style": "Modelin nasıl çizileceğini değiştirir: gölgeli, tel kafes veya gizli kenarlarla.",
"navbar.description.zoom": "Görünümü modele yaklaştırır veya uzaklaştırır.",
"navbar.display": "Görüntü",
"navbar.fit": "Sığdır",
"navbar.graphics": "Grafik",
"navbar.grid": "Izgara",
"navbar.ground_shadow": "Zemin gölgesi",
"navbar.home": "Ana görünüm",
"navbar.later": "Sonra",
"navbar.look_at": "Bak",
"navbar.more": "Daha fazla",
"navbar.orbit": "Yörünge",
"navbar.orthographic": "Ortografik",
"navbar.pan": "Kaydır",
"navbar.perspective": "Perspektif",
"navbar.preset": "Hazır ayar",
"navbar.projection": "Projeksiyon",
"navbar.projection_heading": "Projeksiyon",
"navbar.separator": "Ayırıcı",
"navbar.show": "Göster",
"navbar.spin": "Sürekli döndür",
"navbar.style.shaded": "Gölgeli",
"navbar.style.shaded_with_edges": "Kenarlı gölgeli",
"navbar.style.shaded_with_hidden_edges": "Gizli kenarlı gölgeli",
"navbar.style.wireframe": "Tel kafes",
"navbar.translucent": "Yarı saydam",
"navbar.value_tip": "{name} · {value}",
"navbar.visual_style": "Görsel stil",
"navbar.zoom": "Yakınlaştır",
"needs two joints": "iki bağlantı gerektirir",
"network.allow_once": "Bir kez izin ver",
"network.always_allow": "Bu sunucuya her zaman izin ver",
@@ -1406,6 +1545,11 @@
"no picture": "resim yok",
"none": "yok",
"parts.automatic_name": "otomatik",
"parts.dimension.d": "Nominal çap",
"parts.dimension.height": "Yükseklik",
"parts.dimension.pitch": "Adım",
"parts.dimension.thickness": "Kalınlık",
"parts.dimension.width": "Genişlik",
"pattern.alignment_2": "Hizalama 2",
"pattern.alignment_hint": "Adet, orijinali içerir. Ortala, kopyaları iki yana dağıtır; çift adetlerde yönü ters çevirme, fazladan kopyanın hangi tarafta olacağını belirler.",
"pattern.direction_1": "Yön 1",
@@ -1432,6 +1576,10 @@
"prefs.assistant": "Asistan",
"prefs.axes": "Eksenler",
"prefs.axis": "Eksen",
"prefs.bodies_folders": "Gövde klasörleri",
"prefs.bodies_folders_always": "Her zaman",
"prefs.bodies_folders_never": "Hiçbir zaman",
"prefs.bodies_folders_several": "Yalnızca birden çok gövde",
"prefs.bottom": "Alt",
"prefs.browser": "Tarayıcı",
"prefs.built_in": "Yerleşik",
@@ -1456,6 +1604,10 @@
"prefs.design_note": "Yeni tasarımların neyle başladığı. Açık tasarımlar kendi ayarlarını korur.",
"prefs.devices": "3B fare",
"prefs.dialog_buttons": "Diyalog düğmeleri",
"prefs.dialogs": "Diyaloglar",
"prefs.dialogs_edge": "Görünümün kenarında",
"prefs.dialogs_left_column": "Sol sütunda",
"prefs.dialogs_note": "Komut diyaloglarının açılacağı yer: görünümün kenarına yerleşik veya tarayıcının ve Geçmiş'in yerine sol sütunda; bunlar diyalog kapanınca eskisi gibi geri gelir.",
"prefs.display": "Görüntü",
"prefs.edges": "Kenarlar ve gölgeleme",
"prefs.extend": "Uzat",
@@ -1515,6 +1667,8 @@
"prefs.opaque": "Opak",
"prefs.orbit_snap": "Bir eksene hizalamak için döndürme sırasında Shift tuşunu basılı tutun.",
"prefs.orientation": "Yönelim",
"prefs.origin_plane_names": "Orijin düzlemi adları",
"prefs.origin_plane_names_note": "Tarayıcıda, diyaloglarda ve listelerde gösterilen adlar. Z yukarı olmak üzere {top} XY, {front} XZ ve {right} YZ'dir. Komutlar, betikler ve kayıtlı tasarımlar XY, XZ ve YZ adlarını korur.",
"prefs.overall_speed": "Genel hız",
"prefs.placement": "Yerleşim",
"prefs.prefer_free_sketch_points": "Noktalar örtüştüğünde hareketli eskiz noktalarını tercih et",
@@ -1528,6 +1682,8 @@
"prefs.saving": "Otomatik kayıt ve kurtarma",
"prefs.scale_note": "Pencerede ayrıca Ctrl + / Ctrl −. Düğmeler %100'de 28 px.",
"prefs.scale_text": "Ölçek ve metin",
"prefs.search": "Ayarlarda ara",
"prefs.search_empty": "Eşleşen ayar yok",
"prefs.select": "Seçim",
"prefs.select_one_item": "Bir öğe seçin",
"prefs.selecting": "Seçme",
@@ -1625,6 +1781,7 @@
"render.aging_target_search": "Gövde adlarını filtrele",
"render.aging_upward": "Yukarı bakan birikim",
"render.all_controls_help": "Tüm geçerli kontrollerin yoğun incelemesi. Bölüm sayfaları, temel isteği değiştirmeden odaklı düzenleme sağlar.",
"render.all_shots_label": "Tüm Çekimler",
"render.appearance_physics_separate": "Görsel görünüm yoğunluk, termal veri veya diğer mühendislik özelliklerini atamaz.",
"render.availability_native": "Yerli kompozisyon; uygulamadan önce inceleyin",
"render.availability_projection": "Desteklenen yansıtma; kapsama ve atlamalara bakın",
@@ -1779,10 +1936,13 @@
"render.effects_reset_lens": "Mercek efektlerini sıfırla",
"render.effects_seed": "Desen tohumu",
"render.export_display": "Dışa aktar: görüntü PNG",
"render.export_image_label": "Görüntüyü Dışa Aktar",
"render.export_linear": "Dışa aktar: ham doğrusal EXR",
"render.exr_pending": "GPU ham doğrusal EXR'ı saklayabilir. Sıkıştırma, çok katmanlı görüntüler ve yardımcı render geçişleri kullanılamamaya devam ediyor.",
"render.film_grain": "Fotoğrafik gren",
"render.fit": "Ürünü sığdır",
"render.fixed_camera": "Kamera",
"render.fixed_world": "Dünya",
"render.flare_ghosts": "Kaynak başına hayalet halka",
"render.flare_halo": "Flare halesi",
"render.flare_streak": "Anamorfik tarzı çizgi",
@@ -1809,6 +1969,12 @@
"render.hdr_primaries_srgb": "Renk alanı: sRGB/D65",
"render.hdr_primaries_undeclared": "Kaynak renk alanını beyan etmiyor; işleyici renk gereksinimlerini inceleyin.",
"render.hdr_resource_missing": "Bu tasarımda kaydedilen HDR kaynağı eksik.",
"render.heading_batch": "TOPLU",
"render.heading_frame": "ÇERÇEVE",
"render.heading_lens": "MERCEK",
"render.heading_results": "SONUÇLAR",
"render.heading_shots": "ÇEKİMLER",
"render.hide_shots_label": "Çekim şeridini gizle",
"render.image_advanced": "Gelişmiş GPU kontrolleri",
"render.image_cancelled": "Render iptal edildi",
"render.image_cancelling": "İptal ediliyor",
@@ -1840,6 +2006,7 @@
"render.image_width": "Genişlik",
"render.include_rig_cards": "Pasif kartları ve bayrakları dahil et",
"render.inspect_scene": "Yakalanan sahne özetini yenile",
"render.inspector": "Denetçi",
"render.keep_editing": "Düzenlemeye devam et",
"render.layer_down": "Aşağı taşı",
"render.layer_enabled": "Katman etkin",
@@ -1908,6 +2075,8 @@
"render.library_unfavorite": "Sık kullanılanlardan kaldır",
"render.library_uniform_ack": "Uzamsal veya gelişmiş efektler olmadan tek biçimli yaklaşımı kullan",
"render.library_usable": "Çalıştırılabilir yansıtmalar",
"render.light_anchor_pending": "Işık koordinat çerçevesini değiştirmek henüz desteklenmiyor.",
"render.lights_fixed_to": "Işıklar şuna sabit",
"render.linear_help": "Giderme, pozlama, ton eşleme, filtreler ve arka plandan önce 32 bit float RGBA ana görüntüyü saklar. Doğrusal sRGB/D65; kapsama ile ön çarpımlı. Ek bellek açıkça kabul edilir. Sıkıştırma veya ek geçiş yoktur.",
"render.linear_retain": "Ham doğrusal EXR'ı sakla",
"render.linear_unavailable": "Seçilen tamamlanmış karede saklanmış EXR yok. Render'dan önce saklamayı etkinleştirin; eski bir PNG ham ışınımı geri getiremez.",
@@ -1993,6 +2162,7 @@
"render.output_over_budget": "İstenen görüntü, seçilen kaynak sınırını aşıyor. Uygun bir boyut veya profil açıkça seçin; otomatik düşürme yapılmaz.",
"render.output_ratio_refused": "Bağlı boyut, bu yöntemin boyut sınırını aşıyor. Önceki boyut korunur; oranı kilitten çıkarın veya daha küçük bir değer seçin.",
"render.output_swap_orientation": "Genişlik ve yüksekliği değiştir",
"render.output_tools_inspector": "Boyutu ve biçimi Çıktı denetçisinde düzenleyin.",
"render.page_aging": "Yaşlanma ve kirlenme",
"render.page_all": "Tüm kontroller",
"render.page_appearance": "Görünüm",
@@ -2004,6 +2174,7 @@
"render.page_lighting": "Aydınlatma",
"render.page_output": "Çıktı",
"render.page_scene": "Sahne",
"render.paint_label": "Boya",
"render.passive_stage": "Zemin ve pasif sahne",
"render.passive_stage_help": "Sonlu pasif paneller yalnızca render görüntülerine aittir. CAD gövdesi veya kütle eklemezler. Uygulamadan sonra yerleşim sabittir; ürün sınırlarıyla kesişen reddedilir. CPU dağınık sekme aydınlatması sağlamaz.",
"render.perspective": "Perspektif kamera",
@@ -2019,6 +2190,13 @@
"render.physical_prepare_plan": "Oluşturma planını hazırla (uygulamaz)",
"render.physical_separate": "Görünüm, malzeme kimliği değildir. Bunlar kaynaklı referanslardır, sertifikalı etkin özellikler değildir. Mevcut kütle, mühendislik malzemeleri ve optikler değişmez.",
"render.physical_unknown": "Sayısal aday belirlenmemiş. Kademe, formülasyon veya yapı belirtilmelidir; bilinmez, sıfır değildir.",
"render.pick_body": "Gövde",
"render.pick_component": "Bileşen",
"render.pick_face": "Yüz",
"render.pick_help": "Gövde, Yüz veya Bileşen seçin. Temizlemek için boş alana tıklayın; Shift veya Ctrl ekler ya da seçimi tersine çevirir. Vurgular yalnızca canlı ürün görünümünde görünür.",
"render.pick_label": "Seç",
"render.pick_up_label": "Al",
"render.pick_waiting": "Ürün seçimi henüz desteklenmiyor.",
"render.post_contrast": "Kontrast",
"render.post_gain_b": "Mavi kazancı",
"render.post_gain_g": "Yeşil kazancı",
@@ -2031,6 +2209,7 @@
"render.preview": "Yerleşik önizleme",
"render.preview_help": "Harmanlanmış saydamlıkla raster görüntü. Kırılma, HDRI ve son render henüz kullanılamıyor.",
"render.preview_image_settings_help": "Canlı Önizleme, sabit stüdyo aydınlatmasını korur. Pozlama ve görüntü aydınlatması, Kontrol veya Başlat sırasında görüntü işine uygulanır.",
"render.preview_lighting_help": "Canlı önizleme yalnızca doğrudan ışığı gösterir, gölge göstermez.",
"render.preview_retry": "Canlı önizlemeyi yeniden dene",
"render.preview_unavailable_help": "Canlı önizleme kullanılamıyor. Kaydedilmiş kurulumlar ve görüntü işi kontrolleri erişilebilir kalır. Sahneyi değiştirin veya açıkça yeniden deneyin; reddedilen sahne her karede yeniden kurulmaz.",
"render.preview_upload": "Canlı önizleme tepe noktası yükü",
@@ -2044,8 +2223,10 @@
"render.radial_distortion": "Radyal bozulma",
"render.regrade_action": "Ham ana görüntüyü yeniden derecelendir (giderme yok)",
"render.regrade_help": "Bu sunum kontrollerini kullanarak seçilen tamamlanmış ham ana görüntüden ayrı bir PNG oluşturur. Işın izlemez, yakalanan boyutu/kamerayı/ışıkları değiştirmez veya EXR'ın üzerine yazmaz. Giderme yeniden uygulanmaz: ham ana görüntüler onun kılavuzlarını saklamaz. Türev, aynı iş kontrollerinden dışa aktarılabilir kalır.",
"render.regrade_label": "Yeniden derecelendir",
"render.reload_settings": "Kaydedilmiş ayarları yükle",
"render.replace_ground": "Mevcut sahnenin değiştirilmesini onayla",
"render.replace_label": "Değiştir",
"render.replace_saved_surface": "Bu hedef için mevcut kaydedilmiş bağlamın yerini al",
"render.restore_camera": "Kaydedilmiş kamerayı yükle",
"render.result_actual_help": "Ekran pikseli başına bir görüntü pikseli.",
@@ -2091,6 +2272,7 @@
"render.sampling_title": "Uyarlanabilir ve geçiş örnekleme",
"render.save_continue": "Kaydet ve devam et",
"render.save_setup": "Kurulumu kaydet",
"render.save_shot": "Çekimi Kaydet",
"render.saved_maps_runtime_conflict": "Kaydedilmiş yüzey bağlamları, geçici optik geçersiz kılmalar veya kaydedilmemiş HDR ile karıştırılamaz. HDR'ı kaydedin veya geçici girdileri açıkça kaldırın.",
"render.saved_surface_remove": "Kaydedilmiş bağlamı kaldır",
"render.saved_surfaces": "Kaydedilmiş yüzey bağlamları",
@@ -2110,6 +2292,9 @@
"render.setup_runtime_unsupported": "Bu tam iş henüz kaydedilemez: haritalar veya sahne-gövde bağlamları içeriyor. Taşınabilir bir kurulum kaydetmeden önce o geçici girdileri kaldırın.",
"render.setup_save_missing": "Komut tamamlandıktan sonra kaydedilmiş render yapılandırması bulunamadı.",
"render.sharpening": "Detay keskinleştirme",
"render.shell_tab_appearance": "RENDER GÖRÜNÜMÜ",
"render.shell_tab_camera": "RENDER KAMERASI",
"render.shell_tab_scene": "RENDER SAHNESİ",
"render.shot_duplicate": "Kaydedilmiş çekimi çoğalt",
"render.shot_manager": "Kaydedilmiş çekimler",
"render.shot_new_name": "Yeni çekim adı",
@@ -2118,11 +2303,19 @@
"render.shot_remove_title": "Kaydedilmiş çekim kaldırılsın mı?",
"render.shot_rename": "Kaydedilmiş çekimi yeniden adlandır",
"render.shot_saved_only": "Önce geçerli taslağı kaydedin. Çoğalt ve yeniden adlandır, tam kaydedilmiş seçenekleri ve kaynak referanslarını korur.",
"render.show_shots_label": "Çekim şeridini göster",
"render.sky_ground": "Zemin · doğrusal RGB",
"render.sky_horizon": "Ufuk · doğrusal RGB",
"render.sky_zenith": "Tepe · doğrusal RGB",
"render.soft_focus": "Yumuşak odak",
"render.stage_frame": "Çerçeve",
"render.stage_light": "Işık",
"render.stage_look": "Görünüm",
"render.stage_orthographic": "Ortografik",
"render.stage_output": "Çıktı",
"render.stage_panel_count": "Kaydedilmiş paneller",
"render.stage_preview_note": "Entegre önizleme · doğrudan ışık",
"render.strip_label": "Şerit",
"render.studio_add_area": "Alan ışığı ekle",
"render.studio_ambient": "Sabit ortam ışınımı",
"render.studio_ambient_rgb": "Sabit ortam ışınımı (doğrusal RGB)",
@@ -2179,6 +2372,12 @@
"render.volume_max": "En fazla sınırlar · dünya mm",
"render.volume_min": "En az sınırlar · dünya mm",
"render.volume_title": "Sınırlı homojen hacim",
"render.wait_batch": "henüz desteklenmiyor: kayıtlı çekimlerin render kuyruğu",
"render.wait_compare": "henüz desteklenmiyor: saklanan görüntü karşılaştırması",
"render.wait_paint": "henüz desteklenmiyor: ürün seçimi ve görünüm boyama",
"render.wait_pick_up": "henüz desteklenmiyor: ürün seçimi ve görünüm örnekleme",
"render.wait_replace": "henüz desteklenmiyor: bir görünümün tüm kullanımlarını değiştirme",
"render.wait_solo": "henüz desteklenmiyor: ışık görünürlüğü ve solo denetimleri",
"sample.bracket.about": "2 mm sac metal kuralına göre kenar flanşları ve bir kenet içeren taban flanşı",
"sample.bracket.name": "Sac metal köşebent",
"sample.enclosure.about": "Dudak, vida göbekleri, dayanak ve geçmeli bağlantı içeren, içi boşaltılmış blok",
@@ -2270,6 +2469,7 @@
"timeline.delete_after_marker_one": "Geçmiş İşaretçisinden Sonra 1 Özelliği Sil",
"timeline.delete_after_marker_other": "Geçmiş İşaretçisinden Sonra {count} Özelliği Sil",
"timeline.delete_folders_tip": "Özelliklerini ve iç içe klasörlerini geçmişte tut",
"timeline.delete_folders_with_features_tip": "Klasörleri, içlerindeki her özellikle ve iç içe klasörleriyle birlikte sil",
"timeline.delete_many": "{count} Seçili Özelliği Sil",
"timeline.feature_error": "Hata: {message}",
"timeline.feature_hint": "Düzenlemek için çift tıklayın · sıralamak için sürükleyin · daha fazlası için sağ tıklayın",
@@ -2279,6 +2479,7 @@
"timeline.move_to_group": "Özellikleri klasöre taşı",
"timeline.rename_group": "Özellik klasörünü yeniden adlandır",
"timeline.reorder_many": "Özellikleri yeniden sırala",
"timeline.suppress_folders_tip": "Klasörlerdeki ve iç içe klasörlerindeki her özelliği bastır veya bastırmayı kaldır",
"timeline.ungroup": "Özellik klasörünü kaldır",
"to the face": "yüze kadar",
"to the next face": "sonraki yüze kadar",
@@ -2289,7 +2490,11 @@
"toolbar.render_camera": "KAMERA",
"toolbar.render_environment": "ORTAM",
"toolbar.render_lighting": "AYDINLATMA",
"toolbar.render_lights": "IŞIKLAR",
"toolbar.render_result": "SONUÇ",
"toolbar.render_scene": "SAHNE",
"toolbar.render_stage": "PLATO",
"toolbar.render_studio": "STÜDYO",
"trace.points": "Yol boyunca {count} nokta",
"trace.stopped_by_contact": "Örnek {sample} noktasında biter: {a}, {b} içinden geçecek",
"trace.stopped_by_loop": "Örnek {sample} noktasında biter: {joint} üzerinden geçen döngü daha fazla kapanamaz",
@@ -2468,6 +2673,7 @@
"ui.agent_chat_import_failed": "{name}: içe aktarma başarısız oldu: {error}",
"ui.agent_chat_intro": "Zaten kullandığınız bir kodlama ajanıyla veya seçtiğiniz bir model uç noktasıyla sohbet edin. Ajan, SolidCraft'ın araçlarıyla açık tasarım üzerinde çalışır: Değişiklikleri hemen görünür ve mesaj başına tek adımda geri alınabilir. Sormadan neleri yapabileceğini Ayarlar > AI altında belirlersiniz.",
"ui.agent_chat_key_in_env": "anahtar ayarlı",
"ui.agent_chat_last_missing": "En son kullanılan ajan ({agent}) artık kullanılamıyor. Bir ajan seçin, ardından Yeniden dene'ye basın.",
"ui.agent_chat_left": "Sol",
"ui.agent_chat_limit": "Ajan, bir mesaj için {limit} değişiklik sınırına ulaştı. Devam etmesi için başka bir mesaj gönderin.",
"ui.agent_chat_login_found": "oturum bulundu",
@@ -2514,6 +2720,7 @@
"ui.agent_chat_remove": "Sıradan kaldır",
"ui.agent_chat_remove_file": "Bu iletiden çıkar",
"ui.agent_chat_remove_imported": "Bu iletiden çıkar; parça tasarımda kalır (Geri al onu kaldırır)",
"ui.agent_chat_retry": "Yeniden dene",
"ui.agent_chat_right": "Sağ",
"ui.agent_chat_save": "Kaydet",
"ui.agent_chat_send": "Gönder",
@@ -2526,6 +2733,7 @@
"ui.agent_chat_state_auth": "Oturum açılması gerekiyor",
"ui.agent_chat_state_busy": "Çalışıyor…",
"ui.agent_chat_state_exited": "Ajan sonlandı",
"ui.agent_chat_state_failed": "Bağlantı başarısız",
"ui.agent_chat_state_off": "Bağlı değil",
"ui.agent_chat_state_ready": "Hazır",
"ui.agent_chat_state_starting": "Başlatılıyor…",
@@ -2829,6 +3037,7 @@
"ui.fully_constrained": "Tam kısıtlı",
"ui.function": "Fonksiyon",
"ui.general": "Genel",
"ui.gpu_memory_lowered": "Ekran kartının belleği doldu: görünüm daha düşük kalitede çiziliyor.",
"ui.graphics": "Grafik",
"ui.graphics_custom": "Özel",
"ui.graphics_performance": "Performans",
@@ -2908,6 +3117,7 @@
"ui.heading_utilities": "ARAÇLAR",
"ui.heading_utility": "YARDIMCI ARAÇLAR",
"ui.heading_weldments": "KAYNAK",
"ui.height": "Yükseklik",
"ui.help": "Yardım",
"ui.hide": "Gizle",
"ui.hideAxes": "Eksenleri gizle",
@@ -2950,6 +3160,7 @@
"ui.language": "Dil",
"ui.layout_compact": "Kompakt",
"ui.layout_floating": "Kayan",
"ui.length": "Uzunluk",
"ui.length_decimals": "Uzunluk ondalık basamakları",
"ui.light": "Açık",
"ui.line": "Çizgi",
@@ -3048,6 +3259,7 @@
"ui.pinToShortcuts": "Kısayollara sabitle",
"ui.pinToToolbar": "Araç çubuğuna sabitle",
"ui.pipe": "Boru",
"ui.pitch": "Adım",
"ui.placement": "Yerleşim",
"ui.placement_handles": "Yerleşim tutamaçları",
"ui.placement_handles_tip": "Bir Dayama, Kabarıklık veya Kilit Geçme önizlemesindeki tutamaçlar: işaretçiyi model üzerinde hareket ettirmek için sürükleyin, halkasıyla döndürün",
@@ -3099,6 +3311,7 @@
"ui.render": "Render",
"ui.renderer": "Çizim motoru",
"ui.repeat": "Tekrarla",
"ui.repeat_nothing": "Henüz tekrarlanacak bir şey yok: önce bir araç veya diyalog başlatın.",
"ui.replace_with_link_done": "Kopyalar artık bağlı tasarımın yerleşimleri.",
"ui.report_a_bug": "Hata bildir",
"ui.reset": "Sıfırla",
@@ -3254,6 +3467,7 @@
"ui.visual_shaded_with_edges": "Kenarlarla gölgeli",
"ui.visual_style": "Görsel stil",
"ui.visual_wireframe": "Tel kafes",
"ui.width": "Genişlik",
"ui.window_system": "Pencere sistemi",
"ui.window_system_auto": "Otomatik",
"ui.window_system_wayland": "Wayland",
@@ -3321,5 +3535,6 @@
"web.open_link": "Bağlı tasarım açılsın mı?",
"web.open_link_confirm": "Kaydedilmemiş değişiklikler var. Bağlı tasarım yeni bir sekmede açılsın mı?",
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ seç · Enter / Tab tamamla · Esc kapat",
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ seçin · Enter yazdığınızı uygular · Esc kapatır"
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ seçin · Enter yazdığınızı uygular · Esc kapatır",
"⌘Enter continues on GitHub.": "⌘Enter ile GitHub'da devam edilir."
}
+1138 -921
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1805 -1
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -10,7 +10,7 @@ struct U {
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
acc_v: vec4<f32>,
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
ao: vec4<f32>, // radius in world units (zero: disabled)
ao: vec4<f32>, // x: radius in world units (zero: disabled), y: samples per pixel
// analysis colours (`theme::AnalysisColors`): draft positive, between, negative; reachable,
// blocked; curvature flat, middle, tight
ana_c: array<vec4<f32>, 8>,
@@ -333,9 +333,12 @@ fn occlusion(i: TOut) -> f32 {
// 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.
let flip = reversed();
// Fewer samples while the view moves (#7165), spread over the same hemisphere. 32 is the
// still picture's count, `AO_SAMPLES` in view.rs.
let count = u32(clamp(u.ao.y, 1.0, 32.0));
var blocked = 0.0;
for (var j = 0u; j < 32u; j = j + 1u) {
let f = (f32(j) + 0.5) / 32.0;
for (var j = 0u; j < count; j = j + 1u) {
let f = (f32(j) + 0.5) / f32(count);
let angle = f32(j) * 2.39996323;
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));
@@ -356,7 +359,7 @@ fn occlusion(i: TOut) -> f32 {
blocked = blocked + smoothstep(0.005, 0.15, delta) * (1.0 - smoothstep(0.5, 1.0, delta));
}
}
return 1.0 - 0.85 * blocked / 32.0;
return 1.0 - 0.85 * blocked / f32(count);
}
/// How fast the normal turns across the pixel (per mm).
+24 -4
View File
@@ -8,6 +8,15 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
impl Tokens {
pub const fn light() -> Tokens {
Tokens {
nav_hover: Color32::from_rgb(233, 237, 242),
nav_press: Color32::from_rgb(220, 226, 234),
nav_dim: Color32::from_rgb(136, 149, 167),
nav_viewport: Color32::from_rgb(238, 241, 245),
nav_tip: Color32::from_rgb(27, 36, 48),
nav_tip_ink: Color32::from_rgb(255, 255, 255),
nav_tip_key_border: Color32::from_rgba_unmultiplied_const(128, 140, 160, 153),
nav_field: Color32::from_rgb(255, 255, 255),
borders: crate::theme::Borders::LEGACY,
icon_palette: None,
file_dim: Color32::from_rgb(58, 70, 86),
@@ -184,8 +193,10 @@ impl Tokens {
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),
dimension_driven: Color32::from_rgb(120, 126, 138),
// Sketch dimensions in a plum of their own, apart from the blue and black curves for
// every colour vision mode (#7861).
dimension: Color32::from_rgb(130, 42, 110),
dimension_driven: Color32::from_rgb(160, 90, 120),
sketch_point_free: Color32::from_rgb(30, 90, 200),
section_cap: Color32::from_rgb(214, 128, 72),
dialog_bg: Color32::from_rgb(250, 251, 252),
@@ -314,6 +325,14 @@ impl Tokens {
t.render_ink = Color32::from_rgb(224, 116, 174);
t.render_soft = Color32::from_rgb(64, 35, 54);
t.tab_on_fill_dark = Color32::BLACK;
t.nav_hover = Color32::from_rgb(58, 65, 80);
t.nav_press = Color32::from_rgb(70, 78, 96);
t.nav_dim = Color32::from_rgb(124, 135, 154);
t.nav_viewport = Color32::from_rgb(38, 43, 53);
t.nav_tip = Color32::from_rgb(238, 241, 245);
t.nav_tip_ink = Color32::from_rgb(27, 36, 48);
t.nav_tip_key_border = Color32::from_rgba_unmultiplied_const(128, 140, 160, 153);
t.nav_field = Color32::from_rgb(47, 52, 62);
t.dialog_body = Color32::from_rgb(39, 43, 52);
t.dialog_footer = Color32::from_rgb(47, 52, 62);
t.dialog_rule = Color32::from_rgb(53, 59, 71);
@@ -341,8 +360,9 @@ impl Tokens {
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);
t.dimension_driven = Color32::from_rgb(142, 150, 164);
// Sketch dimensions: Light's plum, lightened for the dark view (#7861).
t.dimension = Color32::from_rgb(224, 176, 224);
t.dimension_driven = Color32::from_rgb(176, 104, 144);
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
t.dialog_bg = Color32::from_rgb(39, 43, 52);
+8
View File
@@ -101,6 +101,14 @@
"mesh_fill": "#ffffff",
"mesh_ink": "#ffffff",
"mesh_soft": "#000000",
"nav_dim": "#ffffff",
"nav_field": "#000000",
"nav_hover": "#000000",
"nav_press": "#ffffff",
"nav_tip": "#ffffff",
"nav_tip_ink": "#000000",
"nav_tip_key_border": "#000000",
"nav_viewport": "#000000",
"on_accent": "#000000",
"origin_plane": "#000000",
"origin_plane_edge": "#ffffff",
+8
View File
@@ -101,6 +101,14 @@
"mesh_fill": "#000000",
"mesh_ink": "#000000",
"mesh_soft": "#ffffff",
"nav_dim": "#000000",
"nav_field": "#ffffff",
"nav_hover": "#ffffff",
"nav_press": "#000000",
"nav_tip": "#000000",
"nav_tip_ink": "#ffffff",
"nav_tip_key_border": "#ffffff",
"nav_viewport": "#ffffff",
"on_accent": "#ffffff",
"origin_plane": "#ffffff",
"origin_plane_edge": "#000000",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#efece8",
"dialog_label": "#403d38",
"dialog_rule": "#d9d5d0",
"dimension": "#403d38",
"dimension_driven": "#6c6863",
"dimension": "#006a70",
"dimension_driven": "#4d8a8c",
"error": "#c71529",
"field": "#fffdfa",
"file_rail": "#e1ddd8",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#100b04",
"dialog_label": "#f3ca87",
"dialog_rule": "#3b280d",
"dimension": "#ffe2a5",
"dimension_driven": "#9b6922",
"dimension": "#f5e050",
"dimension_driven": "#a8962e",
"error": "#fff0ca",
"field": "#100b04",
"form_accent": "#ca892b",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#0c223c",
"dialog_label": "#e3f0fa",
"dialog_rule": "#254764",
"dimension": "#e3f0fa",
"dimension_driven": "#94b2cb",
"dimension": "#ffd970",
"dimension_driven": "#c9a95a",
"error": "#ff968d",
"field": "#071d35",
"form_accent": "#baade8",
@@ -50,8 +50,8 @@
"dialog_footer": "#1e1e2e",
"dialog_label": "#cdd6f4",
"dialog_rule": "#45475a",
"dimension": "#cdd6f4",
"dimension_driven": "#a6adc8",
"dimension": "#94e2d5",
"dimension_driven": "#6c9d95",
"error": "#f38ba8",
"field": "#181825",
"form_accent": "#a9b2f2",
+10 -2
View File
@@ -45,8 +45,8 @@
"dialog_footer": "#000000",
"dialog_label": "#55ffff",
"dialog_rule": "#00aaaa",
"dimension": "#ffffff",
"dimension_driven": "#aaaaaa",
"dimension": "#ffff55",
"dimension_driven": "#aa5500",
"error": "#ff5555",
"field": "#000000",
"file_background": "#000000",
@@ -101,6 +101,14 @@
"mesh_fill": "#55ff55",
"mesh_ink": "#55ff55",
"mesh_soft": "#000000",
"nav_dim": "#55ffff",
"nav_field": "#000000",
"nav_hover": "#aa00aa",
"nav_press": "#ff55ff",
"nav_tip": "#ffffff",
"nav_tip_ink": "#000000",
"nav_tip_key_border": "#55ffff",
"nav_viewport": "#000000",
"on_accent": "#000000",
"origin_plane": "#55ffff20",
"origin_plane_edge": "#55ffff80",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#29241f",
"dialog_label": "#c3bdb5",
"dialog_rule": "#403930",
"dimension": "#c3bdb5",
"dimension_driven": "#9d9285",
"dimension": "#5fc4c0",
"dimension_driven": "#4f9895",
"error": "#f85052",
"field": "#3c352d",
"form_accent": "#a6a2fc",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#f4eee3",
"dialog_label": "#423e36",
"dialog_rule": "#e0d8c9",
"dimension": "#423e36",
"dimension_driven": "#726a5d",
"dimension": "#006a70",
"dimension_driven": "#4a8688",
"error": "#c71529",
"field": "#fdfaf3",
"form_accent": "#5844bc",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#ececec",
"dialog_label": "#262628",
"dialog_rule": "#c6c6c8",
"dimension": "#262628",
"dimension_driven": "#6e6e73",
"dimension": "#00646b",
"dimension_driven": "#3d8087",
"error": "#d53137",
"field": "#ffffff",
"form_accent": "#8b62bf",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#25272b",
"dialog_label": "#e0e2e5",
"dialog_rule": "#484d55",
"dimension": "#e0e2e5",
"dimension_driven": "#a0a6b0",
"dimension": "#5fd0c8",
"dimension_driven": "#4a9a95",
"error": "#ed7777",
"field": "#1c1e21",
"form_accent": "#a08fc4",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#061009",
"dialog_label": "#8def9f",
"dialog_rule": "#173b22",
"dimension": "#8def9f",
"dimension_driven": "#5b9b6a",
"dimension": "#c8ff4d",
"dimension_driven": "#7fa040",
"error": "#d8ffdf",
"field": "#061009",
"form_accent": "#1faa4e",
@@ -55,8 +55,8 @@
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#141617",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
"dimension": "#fabd2f",
"dimension_driven": "#b57614",
"sketch_point": "#ebdbb2",
"sketch_point_free": "#83a598",
"dialog_bg": "#3c3836",
@@ -56,8 +56,8 @@
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
"dimension": "#fabd2f",
"dimension_driven": "#b57614",
"sketch_point": "#ebdbb2",
"sketch_point_free": "#83a598",
"dialog_bg": "#3c3836",
+2 -2
View File
@@ -56,8 +56,8 @@
"sketch_fixed": "#ebdbb2",
"sketch_halo": "#1d2021",
"sketch_locked": "#b8bb26",
"dimension": "#ebdbb2",
"dimension_driven": "#928374",
"dimension": "#fabd2f",
"dimension_driven": "#b57614",
"sketch_point": "#ebdbb2",
"sketch_point_free": "#83a598",
"dialog_bg": "#3c3836",
@@ -55,8 +55,8 @@
"sketch_fixed": "#282828",
"sketch_halo": "#fbf8e3",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
"dimension": "#1f5c40",
"dimension_driven": "#4f7d64",
"sketch_point": "#282828",
"sketch_point_free": "#076678",
"dialog_bg": "#f9f5d7",
@@ -55,8 +55,8 @@
"sketch_fixed": "#282828",
"sketch_halo": "#f6edd0",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
"dimension": "#1f5c40",
"dimension_driven": "#4f7d64",
"sketch_point": "#282828",
"sketch_point_free": "#076678",
"dialog_bg": "#f2e5bc",
+2 -2
View File
@@ -55,8 +55,8 @@
"sketch_fixed": "#282828",
"sketch_halo": "#fcf5d8",
"sketch_locked": "#79740e",
"dimension": "#3c3836",
"dimension_driven": "#7c6f64",
"dimension": "#1f5c40",
"dimension_driven": "#4f7d64",
"sketch_point": "#282828",
"sketch_point_free": "#076678",
"dialog_bg": "#fbf1c7",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#000000",
"dialog_label": "#ffffff",
"dialog_rule": "#a0a0a0",
"dimension": "#ffffff",
"dimension_driven": "#cccccc",
"dimension": "#ffff66",
"dimension_driven": "#c8c850",
"error": "#ff6666",
"field": "#000000",
"form_accent": "#d98af9",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#2e3440",
"dialog_label": "#eceff4",
"dialog_rule": "#4c566a",
"dimension": "#eceff4",
"dimension_driven": "#a4b0c2",
"dimension": "#d08770",
"dimension_driven": "#a87060",
"error": "#bf616a",
"field": "#242933",
"form_accent": "#cfa7c7",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#282a36",
"dialog_label": "#f8f8f2",
"dialog_rule": "#50546d",
"dimension": "#f8f8f2",
"dimension_driven": "#a1a4c4",
"dimension": "#8be9fd",
"dimension_driven": "#62a3b1",
"error": "#ff5555",
"field": "#21222c",
"form_accent": "#c096e8",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#1b1e20",
"dialog_label": "#bbbec1",
"dialog_rule": "#2f3235",
"dimension": "#bbbec1",
"dimension_driven": "#8f9296",
"dimension": "#5fc4c0",
"dimension_driven": "#4f9895",
"error": "#f85052",
"field": "#2c2e31",
"file_rail": "#0f1113",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#2f343e",
"dialog_label": "#b4bbc8",
"dialog_rule": "#353b47",
"dimension": "#abb2bf",
"dimension_driven": "#7f848e",
"dimension": "#56b6c2",
"dimension_driven": "#3f8590",
"error": "#e06c75",
"field": "#1e2227",
"form_accent": "#d283e9",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#dddddd",
"dialog_label": "#222222",
"dialog_rule": "#aaaaaa",
"dimension": "#222222",
"dimension_driven": "#666666",
"dimension": "#00787f",
"dimension_driven": "#4d9196",
"error": "#bb3333",
"field": "#ffffff",
"form_accent": "#704e81",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#eee8d5",
"dialog_label": "#586e75",
"dialog_rule": "#b6b3a1",
"dimension": "#586e75",
"dimension_driven": "#657b83",
"dimension": "#136059",
"dimension_driven": "#3d8f88",
"error": "#dc322f",
"field": "#fdf6e3",
"form_accent": "#6b70c3",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#c0c0c0",
"dialog_label": "#000000",
"dialog_rule": "#808080",
"dimension": "#000000",
"dimension_driven": "#808080",
"dimension": "#ffffff",
"dimension_driven": "#ffff00",
"error": "#ff0000",
"field": "#ffffff",
"form_accent": "#800080",
+2 -2
View File
@@ -50,8 +50,8 @@
"dialog_footer": "#1a1b26",
"dialog_label": "#c0caf5",
"dialog_rule": "#414868",
"dimension": "#c0caf5",
"dimension_driven": "#9aa5ce",
"dimension": "#73daca",
"dimension_driven": "#4f9a90",
"error": "#f7768e",
"field": "#16161e",
"form_accent": "#b190ec",
+43 -17
View File
@@ -1,15 +1,40 @@
//! Lists the design-format changes in `src/format/`: one file per change, in `v<N>/` once a
//! release fixed its number, else in `pending/` (see `src/format/pending/README.md`). The list is
//! Lists the format changes of each family of files versioned by `src/format.rs`: designs in
//! `src/format/`, drawings in `src/drawing/format/` (#3890). One file per change, in `v<N>/` once a
//! release fixed its number, else in `pending/` (see `src/format/pending/README.md`). The lists are
//! generated so that a change adds a file and never edits a line another change also edits.
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
/// The last format whose upgrade steps are written out in `format.rs` (`JSON_STEPS`).
const HAND_WRITTEN: u32 = 22;
/// A family's change files: their folder, the last format before them, and the prefix of the
/// family's lists in `format.rs`.
struct Family {
folder: &'static str,
base: u32,
list_prefix: &'static str,
}
/// Every family, each read by `scripts/fold-format-pending.sh` too. A design's upgrade steps are
/// written out in `format.rs` up to 22 (`JSON_STEPS`); a drawing's first format is 1.
const FAMILIES: [Family; 2] =
[Family { folder: "src/format", base: 22, list_prefix: "" }, Family { folder: "src/drawing/format", base: 1, list_prefix: "DRAWING_" }];
fn main() {
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/format");
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let mut out = String::new();
let mut mods = String::new();
for family in &FAMILIES {
lists(family, &manifest, &mut out, &mut mods);
}
out.push_str(&mods);
let dest = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("format_parts.rs");
std::fs::write(dest, out).unwrap();
}
/// A family's `FOLDED` and `PENDING` lists; its folded formats follow on from its base.
fn lists(family: &Family, manifest: &Path, out: &mut String, mods: &mut String) {
let Family { folder, base, list_prefix } = family;
let root = manifest.join(folder);
println!("cargo:rerun-if-changed={}", root.display());
let mut folded: Vec<(u32, PathBuf)> = std::fs::read_dir(&root)
.unwrap_or_else(|e| panic!("{}: {e}", root.display()))
@@ -21,23 +46,24 @@ fn main() {
.collect();
folded.sort();
for (i, (version, _)) in folded.iter().enumerate() {
let expected = HAND_WRITTEN + 1 + i as u32;
assert_eq!(*version, expected, "src/format/: expected v{expected}/ next, found v{version}/ (versions are contiguous)");
let expected = base + 1 + i as u32;
assert_eq!(*version, expected, "{folder}/: expected v{expected}/ next, found v{version}/ (versions are contiguous)");
}
let mut out = String::new();
let mut mods = String::new();
out.push_str("/// The changes of each format after the hand-written ones, oldest first.\nconst FOLDED: &[(u32, &[Part])] = &[\n");
// Module names start with the family's (`drawing_pending_…`), so two families' changes of one
// name never clash.
let module = list_prefix.to_ascii_lowercase();
let _ = writeln!(
out,
"/// The changes of each format after the base, oldest first (`{folder}/v<N>/`).\nconst {list_prefix}FOLDED: &[(u32, &[Part])] = &["
);
for (version, dir) in &folded {
let _ = writeln!(out, " ({version}, &[");
parts(dir, &format!("v{version}"), &mut out, &mut mods);
parts(dir, &format!("{module}v{version}"), out, mods);
out.push_str(" ]),\n");
}
out.push_str("];\n\n/// The changes no release has a number for yet (`src/format/pending/`).\nconst PENDING: &[Part] = &[\n");
parts(&root.join("pending"), "pending", &mut out, &mut mods);
out.push_str("];\n");
out.push_str(&mods);
let dest = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("format_parts.rs");
std::fs::write(dest, out).unwrap();
let _ = writeln!(out, "];\n\n/// The changes no release has a number for yet (`{folder}/pending/`).\nconst {list_prefix}PENDING: &[Part] = &[");
parts(&root.join("pending"), &format!("{module}pending"), out, mods);
out.push_str("];\n\n");
}
/// The `Part`s of the change files in `dir`, in file-name order, and a module for each.
+350 -20
View File
@@ -3,7 +3,7 @@
//! a component's over the physical material's look; a body with none of these keeps the colour
//! it was imported with.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use solidcraft_geom::Vec3;
@@ -129,6 +129,95 @@ impl Appearances {
pub fn is_empty(&self) -> bool {
self.bodies.is_empty() && self.faces.is_empty() && self.components.is_empty()
}
/// Bound bulk appearance operations, including designs read from untrusted files.
pub fn check_budget(&self) -> crate::Result<()> {
if self.bodies.len().saturating_add(self.faces.len()).saturating_add(self.components.len()) > 100_000 {
return Err(crate::DocError::Invalid("at most 100000 appearance overrides can be processed".into()));
}
Ok(())
}
/// Build a replacement without changing the source. Equality includes the label and
/// every optical value. Physical-material defaults are deliberately outside this scope.
/// `None` means no change, allowing callers to preserve history and revision.
pub fn replacing(&self, from: &Look, to: &Look, scope: Option<&AppearanceScope>) -> crate::Result<Option<(Self, usize)>> {
self.check_budget()?;
for look in [from, to] {
if !look.opacity.is_finite() || !(0.0..=1.0).contains(&look.opacity) {
return Err(crate::DocError::Invalid("opacity must be finite and in 0..1".into()));
}
look.opaque_surface()?;
}
if from == to {
return Ok(None);
}
let body_in_scope = |body: &str| scope.is_none_or(|s| s.bodies.contains(body));
let component_in_scope = |id: &u64| scope.is_none_or(|s| s.components.contains(id));
let count = self.bodies.iter().filter(|(body, look)| body_in_scope(body) && *look == from).count()
+ self.faces.iter().filter(|face| body_in_scope(&face.body) && &face.look == from).count()
+ self.components.iter().filter(|(id, look)| component_in_scope(id) && *look == from).count();
if count == 0 {
return Ok(None);
}
let mut replaced = self.clone();
for (body, look) in &mut replaced.bodies {
if body_in_scope(body) && look == from {
*look = to.clone();
}
}
for face in &mut replaced.faces {
if body_in_scope(&face.body) && &face.look == from {
face.look = to.clone();
}
}
for (id, look) in &mut replaced.components {
if component_in_scope(id) && look == from {
*look = to.clone();
}
}
Ok(Some((replaced, count)))
}
}
/// Exact override containers for replacement. A body includes its face overrides;
/// a component selects its own override, independently of the bodies it owns.
#[derive(Clone, Debug, Default)]
pub struct AppearanceScope {
pub bodies: BTreeSet<String>,
pub components: BTreeSet<u64>,
}
/// A target in an evaluated model. Face indices are those of `model.faces`.
#[derive(Clone, Copy, Debug)]
pub enum AppearanceTarget<'a> {
Body(&'a str),
Face { body: &'a str, face: usize },
Component(u64),
}
/// The four override steps, in descending precedence. Imported paint is a display
/// fallback, not an override or physical material, and is not a fifth step.
#[derive(Clone, Debug, PartialEq)]
pub struct AppearanceChain {
pub face: Option<Look>,
pub body: Option<Look>,
pub component: Option<Look>,
pub material: Option<Look>,
}
impl AppearanceChain {
pub fn steps(&self) -> [(&'static str, Option<&Look>); 4] {
[("face", self.face.as_ref()), ("body", self.body.as_ref()), ("component", self.component.as_ref()), ("material", self.material.as_ref())]
}
pub fn active(&self) -> Option<&'static str> {
self.steps().into_iter().find_map(|(step, look)| look.map(|_| step))
}
pub fn resolved(&self) -> Option<&Look> {
self.steps().into_iter().find_map(|(_, look)| look)
}
}
/// The built-in appearance library: (name, colour, opacity).
@@ -161,15 +250,96 @@ pub fn material_look(material: &str) -> Option<Look> {
}
impl Document {
/// Read the same precedence used by body and face display resolution. Components
/// have no material assignment of their own: the material step is present only
/// when their directly owned bodies all have the same material look. Empty or
/// mixed components have no material step. Root is not an assignable component.
pub fn appearance_chain(&self, state: &crate::ModelState, target: AppearanceTarget<'_>) -> crate::Result<AppearanceChain> {
self.appearances.check_budget()?;
match target {
AppearanceTarget::Body(name) | AppearanceTarget::Face { body: name, .. } => {
let body = state.body(name).ok_or_else(|| crate::DocError::Invalid(format!("no body `{name}`")))?;
let mut chain = self.body_appearance_chain(body.definition_name(), self.model_body_component(body));
if let AppearanceTarget::Face { face, .. } = target {
if face >= body.body.face_count() {
return Err(crate::DocError::Invalid("unknown face index".into()));
}
let mut overrides = Vec::new();
self.apply_face_overrides(body, &mut overrides);
chain.face = overrides.into_iter().find_map(|(index, look)| (index == face).then_some(look));
}
Ok(chain)
}
AppearanceTarget::Component(id) => {
if id == 0 || !self.components.iter().any(|c| c.id == id) {
return Err(crate::DocError::Invalid("unknown or root component".into()));
}
if state.bodies.len() > 100_000 {
return Err(crate::DocError::Invalid("too many bodies to read a component appearance chain".into()));
}
// Legacy ownership can refer to a feature. Index it once, rather
// than scanning the timeline for each body of a large component.
let features: BTreeMap<_, _> = self.features.iter().rev().map(|f| (f.id, f.component)).collect();
let owner = |b: &&crate::ModelBody| {
if b.transferred || b.source_name.is_some() {
b.component
} else {
self.body_components.get(&b.name).copied().or_else(|| features.get(&b.feature).copied()).unwrap_or(0)
}
};
let mut materials = state
.bodies
.iter()
.filter(|b| owner(b) == id)
.map(|b| self.materials.get(b.definition_name()).and_then(|m| self.material_look(m)));
let material = materials.next().flatten().filter(|first| materials.all(|look| look.as_ref() == Some(first)));
Ok(AppearanceChain { face: None, body: None, component: self.appearances.components.get(&id).cloned(), material })
}
}
}
fn body_appearance_chain(&self, name: &str, component: u64) -> AppearanceChain {
AppearanceChain {
face: None,
body: self.appearances.bodies.get(name).cloned(),
component: (component != 0).then(|| self.appearances.components.get(&component).cloned()).flatten(),
material: self.materials.get(name).and_then(|material| self.material_look(material)),
}
}
/// Resolve a picked face through the same appearance chain used by the viewport.
pub fn model_face_look(&self, body: &crate::ModelBody, face: usize) -> Option<Look> {
self.face_colors(body).into_iter().find(|(index, _)| *index == face).map(|(_, look)| look).or_else(|| self.model_display_look(body))
}
/// Body appearance including the imported fallback used by `body_color`.
pub fn model_display_look(&self, body: &crate::ModelBody) -> Option<Look> {
self.model_body_look(body).or_else(|| {
let color = body.body.color().or_else(|| body.imported.as_ref().and_then(|p| p.color))?;
Some(Look::custom(color.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8), self.model_body_opacity(body)))
})
}
/// Distinct appearances used by an object, including its face overrides. `None` is
/// the unassigned/imported body appearance. Repeated faces count as one object user.
pub fn model_used_looks(&self, body: &crate::ModelBody) -> Vec<Option<Look>> {
let faces = self.face_colors(body);
let mut looks = Vec::new();
if faces.len() < body.body.face_count() || faces.is_empty() {
looks.push(self.model_display_look(body));
}
for (_, look) in faces {
let look = Some(look);
if !looks.contains(&look) {
looks.push(look);
}
}
looks
}
/// Shared definition appearance for a computed occurrence body.
pub fn model_body_look(&self, body: &crate::ModelBody) -> Option<Look> {
let name = body.definition_name();
self.appearances
.bodies
.get(name)
.cloned()
.or_else(|| self.appearances.components.get(&self.model_body_component(body)).cloned())
.or_else(|| self.materials.get(name).and_then(|material| self.material_look(material)))
self.body_appearance_chain(body.definition_name(), self.model_body_component(body)).resolved().cloned()
}
/// Effective body opacity, including paint carried from an imported body.
pub fn model_body_opacity(&self, body: &crate::ModelBody) -> f64 {
@@ -182,15 +352,7 @@ impl Document {
/// A body's look (without face appearances): its own, else its component's, else its
/// material's.
pub fn body_look(&self, name: &str, feature: u64) -> Option<Look> {
self.appearances
.bodies
.get(name)
.cloned()
.or_else(|| {
let c = self.body_component(name, feature);
(c != 0).then(|| self.appearances.components.get(&c).cloned()).flatten()
})
.or_else(|| self.materials.get(name).and_then(|m| self.material_look(m)))
self.body_appearance_chain(name, self.body_component(name, feature)).resolved().cloned()
}
/// Face appearances on a body: (point on the face, look).
@@ -201,9 +363,19 @@ impl Document {
/// The faces of a body with appearances of their own: (B-rep face index, look). A later
/// assignment to the same face wins.
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
let mut out = self.imported_face_colors(b);
for (index, look) in self.authored_face_colors(b) {
out.retain(|(old, _)| *old != index);
out.push((index, look));
}
out
}
/// Imported paint alone, after applying body-appearance precedence.
pub(crate) fn imported_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
// An assigned body appearance replaces imported paint; design face appearances still win.
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
let mut out: Vec<(usize, Look)> = if self.appearances.bodies.contains_key(b.definition_name()) {
if self.appearances.bodies.contains_key(b.definition_name()) {
Vec::new()
} else {
match b.body.paint() {
@@ -219,7 +391,18 @@ impl Document {
None => Vec::new(),
},
}
};
}
}
/// Design-authored face overrides, without imported STEP paint. Linked designs carry
/// these independently so an inherited body look does not hide their face overrides.
pub fn authored_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
let mut out = Vec::new();
self.apply_face_overrides(b, &mut out);
out
}
fn apply_face_overrides(&self, b: &crate::ModelBody, out: &mut Vec<(usize, Look)>) {
for f in self.appearances.faces.iter().filter(|f| f.body == b.definition_name()) {
let (p, look) = (crate::apply_point(&b.source_transform, f.point), f.look.clone());
if f.strict_name {
@@ -259,7 +442,6 @@ impl Document {
out.push((i, look));
}
}
out
}
}
@@ -385,6 +567,28 @@ mod tests {
assert!(restored.iter().all(|(face, look)| *look == if *face == 0 { face_look.clone() } else { Look::custom([255, 0, 0], 0.5) }));
}
#[test]
fn linked_paint_retains_imported_faces_without_baking_authored_overrides() {
let mut state = crate::ModelState::default();
state.bodies.push(imported_box());
let body = &state.bodies[0];
let mut doc = Document::default();
doc.appearances.faces.push(FaceLook {
body: body.name.clone(),
point: Vec3::ZERO,
look: Look::custom([0, 255, 0], 1.0),
name: Some(crate::naming::face_names(body)[0].clone()),
strict_name: false,
certificate: None,
});
let exported = doc.painted(&state);
assert!(exported.bodies[0].body.paint().unwrap().faces.iter().any(|face| face.color == [0., 1., 0.]));
let linked = doc.painted_for_link(&state);
let faces = &linked.bodies[0].body.paint().unwrap().faces;
assert_eq!(faces.len(), body.body.faces(0.01).unwrap().len());
assert!(faces.iter().all(|face| face.color == [1., 0., 0.] && face.opacity == 0.5));
}
#[test]
fn body_appearance_overrides_imported_face_paint() {
check_imported_appearance_precedence(&imported_box());
@@ -400,6 +604,130 @@ mod tests {
check_imported_appearance_precedence(&body);
}
fn chain_fixture() -> (Document, crate::ModelState) {
let mut doc = Document::new("Appearance chain");
doc.components.push(crate::Component { id: 1, name: "Part".into(), ..Default::default() });
doc.body_components.insert("B".into(), 1);
let mut state = crate::ModelState::default();
state.bodies.push(crate::ModelBody::new("B".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap(), 1));
(doc, state)
}
#[test]
fn appearance_chain_agrees_with_display_and_definition_identity() {
let (mut doc, mut state) = chain_fixture();
let body_look = library("ABS - Red").unwrap();
let face_look = library("Steel - Satin PBR").unwrap();
doc.appearances.bodies.insert("B".into(), body_look.clone());
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
doc.materials.insert("B".into(), "Brass".into());
let body = &state.bodies[0];
let spot = crate::naming::face_spots(body)[0].unwrap().point;
let face_name = crate::naming::face_names(body)[0].clone();
doc.appearances.faces.push(FaceLook {
body: "B".into(),
point: spot,
look: face_look.clone(),
name: Some(face_name),
strict_name: false,
certificate: None,
});
let chain = doc.appearance_chain(&state, AppearanceTarget::Face { body: "B", face: 0 }).unwrap();
assert_eq!(chain.active(), Some("face"));
assert_eq!(chain.resolved(), Some(&face_look));
assert_eq!(doc.model_face_look(body, 0).as_ref(), chain.resolved());
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
assert_eq!(chain.active(), Some("body"));
assert_eq!(doc.model_body_look(body).as_ref(), chain.resolved());
assert_eq!(doc.body_look("B", 1).as_ref(), chain.resolved());
assert_eq!(chain.component.as_ref().unwrap().name, "ABS - Blue");
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
// A computed placement resolves the definition's assignments, not its label.
state.bodies[0].name = "Occurrence/B".into();
state.bodies[0].source_name = Some("B".into());
state.bodies[0].component = 1;
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("Occurrence/B")).unwrap();
assert_eq!(chain.body, Some(body_look));
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
assert!(doc.appearance_chain(&state, AppearanceTarget::Face { body: "Occurrence/B", face: usize::MAX }).is_err());
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("missing")).is_err());
}
#[test]
fn appearance_chain_component_material_is_common_or_absent_and_imports_stay_fallbacks() {
let (mut doc, mut state) = chain_fixture();
state.bodies[0].imported = Some(std::sync::Arc::new(ImportedLook { color: Some([0.2, 0.4, 0.6]), ..Default::default() }));
let body = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
assert_eq!(body.active(), None);
assert!(doc.model_display_look(&state.bodies[0]).is_some());
doc.materials.insert("B".into(), "Brass".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
let mut second = state.bodies[0].clone();
second.name = "C".into();
doc.body_components.insert("C".into(), 1);
state.bodies.push(second);
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), None);
doc.materials.insert("C".into(), "Brass".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
doc.materials.insert("C".into(), "Steel".into());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("component"));
state.bodies.clear();
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(0)).is_err());
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(99)).is_err());
}
#[test]
fn appearance_replacement_validates_values_and_design_budget_without_mutation() {
let from = library("ABS - Red").unwrap();
let to = library("ABS - Blue").unwrap();
let mut appearances = Appearances::default();
appearances.bodies.insert("B".into(), from.clone());
for opacity in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1., 2.] {
let mut invalid = to.clone();
invalid.opacity = opacity;
assert!(appearances.replacing(&from, &invalid, None).is_err());
assert!(appearances.replacing(&invalid, &to, None).is_err());
assert_eq!(appearances.bodies["B"], from);
}
let mut invalid = library("Steel - Satin PBR").unwrap();
invalid.opacity = 0.5;
assert!(appearances.replacing(&from, &invalid, None).is_err());
let (changed, count) = appearances.replacing(&from, &to, None).unwrap().unwrap();
assert_eq!(count, 1);
assert_eq!(changed.bodies["B"], to);
assert_eq!(appearances.bodies["B"], from);
assert!(appearances.replacing(&from, &from, None).unwrap().is_none());
for i in 0..100_000 {
appearances.components.insert(i, from.clone());
}
assert!(appearances.replacing(&from, &to, None).is_err());
let (mut doc, state) = chain_fixture();
doc.appearances = appearances;
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("B")).is_err());
}
#[test]
fn appearance_chain_of_ten_thousand_body_component_has_four_steps() {
let (mut doc, mut state) = chain_fixture();
let mut body = state.bodies[0].clone();
body.source_name = Some("B".into());
body.component = 1;
state.bodies = (0..10_000)
.map(|i| {
let mut b = body.clone();
b.name = format!("Placed {i}");
b
})
.collect();
doc.materials.insert("B".into(), "Brass".into());
let chain = doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap();
assert_eq!(chain.steps().len(), 4);
assert_eq!(chain.active(), Some("material"));
}
#[test]
fn library_and_materials() {
assert_eq!(library("abs - red").map(|l| l.color), Some([196, 40, 40]));
@@ -487,6 +815,8 @@ mod tests {
bad.boundary = GlassBoundary::Thin { thickness_mm: 10001.0 };
assert!(bad.validate().is_err());
}
// The swatch shows the transmission as sRGB codes.
assert_eq!(find("starter.glass.smoky-neutral").unwrap().preview_color(), [160, 166, 173]);
}
#[test]
+1 -4
View File
@@ -154,10 +154,7 @@ impl VisualPreset {
match self.recipe {
Recipe::Opaque(r) => r.color,
// A UI swatch of the transmission values, not rendered glass or absorption.
Recipe::Glass(r) => r.transmission_linear_rgb.map(|v| {
let encoded = if v <= 0.0031308 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
(encoded.clamp(0.0, 1.0) * 255.0).round() as u8
}),
Recipe::Glass(r) => r.transmission_linear_rgb.map(solidcraft_geom::srgb::encode8),
}
}
+42
View File
@@ -124,6 +124,48 @@ fn identity() -> Mat {
IDENTITY
}
/// How a component counts in the bill of materials of an assembly it is placed in. It is a part
/// property (the `bom_type` key of the component's [`Metadata`](crate::Metadata)); a component
/// without one is [`BomType::Normal`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BomType {
/// A row of its own; every occurrence adds to its quantity.
#[default]
Normal,
/// No row of its own: what it holds counts as if placed directly in its parent.
Phantom,
/// Shown, but counted nowhere, and nothing inside it is counted either.
Reference,
/// Bought: one row, with nothing inside it counted or listed.
Purchased,
/// Made as one item that doesn't come apart (a welded frame): one row in the assembly it is
/// placed in. What it holds is listed in its own bill of materials, and its purchased parts
/// also in the parts-only list of the assembly it is placed in.
Inseparable,
}
impl BomType {
/// Every type, in the order commands and documentation list them.
pub const ALL: [BomType; 5] = [BomType::Normal, BomType::Phantom, BomType::Reference, BomType::Purchased, BomType::Inseparable];
/// The word the design file and commands use.
pub fn as_str(self) -> &'static str {
match self {
BomType::Normal => "normal",
BomType::Phantom => "phantom",
BomType::Reference => "reference",
BomType::Purchased => "purchased",
BomType::Inseparable => "inseparable",
}
}
/// The type a word names, if it names one.
pub fn parse(word: &str) -> Option<BomType> {
BomType::ALL.into_iter().find(|t| t.as_str() == word)
}
}
/// How deep and how many placements [`Document::placements`] walks.
pub const MAX_PLACEMENT_DEPTH: usize = 64;
pub const MAX_PLACEMENTS: usize = 100_000;
+43 -6
View File
@@ -2617,13 +2617,42 @@ pub struct StandardPart {
/// A component whose contents come from another design file (Insert Design): where that file
/// is, and the Import feature in the component that holds its bodies (Update Links replaces
/// them; Break Link keeps them and forgets the file).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct DesignLink {
/// The design file: relative to this design's folder once this design is saved, else as
/// given (`/` separates folders).
pub path: String,
/// The Import feature holding the linked design's bodies.
pub feature: u64,
/// Last inherited values, by Import body slot. Differing assembly assignments are
/// local overrides and survive a source update. Older links start with no baseline.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inherited: Vec<LinkedBodyProperties>,
}
/// The values last inherited by a linked body, not another editable material library.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LinkedBodyProperties {
pub material: String,
/// SHA-256 of the inherited custom definition, for equality only. The complete
/// engineering card stays in user_materials, not duplicated once per linked body.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub material_fingerprint: Option<String>,
/// Explicit body/component look only: material fallback must remain a fallback.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub appearance: Option<crate::appearance::Look>,
/// Last inherited authored face overrides. None marks links saved before face
/// inheritance; Some(empty) records that the source has no authored face overrides.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub faces: Option<Vec<crate::appearance::FaceLook>>,
}
impl LinkedBodyProperties {
pub fn fingerprint(material: &UserMaterial) -> Result<String> {
use sha2::{Digest, Sha256};
let bytes = serde_json::to_vec(material).map_err(|e| DocError::Invalid(e.to_string()))?;
Ok(format!("{:x}", Sha256::digest(bytes)))
}
}
/// A name the design's counter hands out (`Body1`).
@@ -3131,14 +3160,22 @@ impl Document {
/// The model with each body carrying the colour, opacity and face colours it is shown with
/// (for export).
pub fn painted(&self, state: &crate::ModelState) -> crate::ModelState {
self.painted_with_faces(state, true)
}
/// Linked designs carry authored face overrides as editable properties. Their STEP
/// geometry keeps imported paint, without baking a second copy of those overrides.
pub fn painted_for_link(&self, state: &crate::ModelState) -> crate::ModelState {
self.painted_with_faces(state, false)
}
fn painted_with_faces(&self, state: &crate::ModelState, authored_faces: bool) -> crate::ModelState {
let mut st = state.clone();
for b in &mut st.bodies {
let c = self.body_color(b);
let faces: Vec<solidcraft_kernel::FacePaint> = self
.face_colors(b)
.into_iter()
.map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
.collect();
let face_colors = if authored_faces { self.face_colors(b) } else { self.imported_face_colors(b) };
let faces: Vec<solidcraft_kernel::FacePaint> =
face_colors.into_iter().map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 }).collect();
let opacity = self.model_body_opacity(b) as f32;
let paint = solidcraft_kernel::Paint { opacity, faces };
if c != b.body.color() || b.body.paint() != Some(&paint) {
+305
View File
@@ -0,0 +1,305 @@
//! A drawing (#3890): sheets of paper that draw designs saved in their own files. A drawing is a
//! file of its own, `"format": "solidcraft-drawing/N"`, versioned by the same code as designs
//! ([`DRAWING`]); format 1 is this record.
//!
//! - **Sources** are the designs it draws: an id and the design file's path, relative to the
//! drawing's folder once the drawing is saved, else as given, with `/` between folders (a linked
//! design's [`DesignLink`](crate::DesignLink) rule). What a design was when the drawing last saw
//! it is not saved here.
//! - **Sheets**: the paper, a copy of its drawing border, the resolution its pictures are rendered
//! at, the component it documents, and whether it is left out of the sheet count or of printing.
//! The border is a copy of the profile it was made with, never a name, so a later built-in never
//! changes a saved drawing.
//! - **Units** follow each view's design or are one length unit for the whole drawing. The
//! **language** is the content's, a BCP 47 tag chosen when the drawing is made, whatever the
//! interface's is. **Properties** follow [`Metadata`]'s rules.
//!
//! The rules, which every later kind of record (views, annotations, tables) keeps too:
//! - Every object has an id of [`crate::ids`]: random, so objects added side by side never take
//! the same one, and written as 13 characters of base32. No two objects share an id, and a run
//! never gives a removed object's id again ([`Drawing::allocate_id`]).
//! - Every reference between records is an id and must resolve: a file with one that doesn't is
//! damaged and refused, never patched.
//! - 1 to [`MAX_SHEETS`] sheets and at most [`MAX_SOURCES`] sources; names of 1 to
//! [`MAX_NAME_BYTES`] bytes; paths of 1 to [`MAX_PATH_BYTES`] bytes, without NUL.
//! - Nothing derived is saved, and a drawing always saves as the same bytes: fields in their
//! order, maps sorted.
//! - A later change to the record is one file in `src/drawing/format/pending/` (its README).
//!
//! [`Drawing::from_json`] reads a file in this order: at most [`MAX_DRAWING_BYTES`] of text, the
//! family's format check, every number in range, the upgrade, the typed record, then
//! [`Drawing::validate`].
use std::collections::BTreeSet;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solidcraft_geom::drawing_sheet::{self, DrawingBorderProfile};
use crate::format::DRAWING;
use crate::ids::{self, IdSource};
use crate::{DocError, Metadata, Result, expr};
/// The most sheets a drawing may have.
pub const MAX_SHEETS: usize = 1000;
/// The most designs a drawing may draw.
pub const MAX_SOURCES: usize = 64;
/// The longest name of a drawing or a sheet, in bytes.
pub const MAX_NAME_BYTES: usize = 256;
/// The longest path to a design, in bytes.
pub const MAX_PATH_BYTES: usize = 4096;
/// The longest language tag: what RFC 5646 asks every reader to take (§4.4.1).
pub const MAX_LANGUAGE_CHARS: usize = 35;
/// The largest drawing read, as JSON text.
pub const MAX_DRAWING_BYTES: usize = 16 << 20;
/// The resolution a sheet's pictures (its shaded views) are rendered at unless it says, in dots
/// per inch.
pub const DEFAULT_DPI: f64 = 300.0;
/// The lowest resolution a sheet may ask for.
pub const MIN_DPI: f64 = 25.0;
/// The highest resolution a sheet may ask for.
pub const MAX_DPI: f64 = 2400.0;
/// A drawing: its sheets and the designs they draw.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Drawing {
/// `solidcraft-drawing/N`: the format this build writes, whatever the file said.
pub format: String,
/// The changes of `format` this drawing has while they are pending
/// ([`Family::pending_names`](crate::format::Family::pending_names)). Empty, and not saved,
/// otherwise.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub format_pending: Vec<String>,
pub name: String,
/// The designs the drawing draws.
pub sources: Vec<Source>,
/// The drawing's properties and other tools' data, kept as they are.
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
pub properties: Metadata,
#[serde(default)]
pub units: Units,
/// The language of the drawing's content, a BCP 47 tag (`en-GB`).
pub language: String,
/// In order, each behind its own `Arc`, so an undo step shares every sheet it didn't change.
pub sheets: Vec<Arc<Sheet>>,
}
/// A design the drawing draws.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Source {
#[serde(with = "crate::ids")]
pub id: u64,
/// The design file: relative to the drawing's folder once the drawing is saved, else as given
/// (`/` separates folders).
pub path: String,
}
/// The length unit a drawing gives its measurements in.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Units {
/// Each view's design's own units.
#[default]
FollowModel,
/// One length unit for the whole drawing, by its name in [`expr::UNITS`] (`mm`, `in`).
Fixed { unit: String },
}
/// One sheet of a drawing.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Sheet {
#[serde(with = "crate::ids")]
pub id: u64,
pub name: String,
/// The paper: its size and the way it lies.
pub sheet: drawing_sheet::Sheet,
/// A copy of the drawing border's profile (`None`: no drawing border).
pub border: Option<DrawingBorderProfile>,
/// The resolution its pictures (its shaded views) are rendered at, in dots per inch.
#[serde(default = "default_dpi")]
pub dpi: f64,
/// The component the sheet documents, which its title block reads.
pub documents: Option<SheetComponent>,
#[serde(default)]
pub excluded: Excluded,
}
/// A component of one of the drawing's designs.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SheetComponent {
/// The [`Source`]'s id.
#[serde(with = "crate::ids")]
pub source: u64,
/// The component's id in that design, as the design writes it (0: the design itself).
pub component: u64,
}
/// What a sheet is left out of.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Excluded {
/// The sheet count ("sheet 2 of 3" on the other sheets).
pub count: bool,
/// Printing and exporting every sheet.
pub print: bool,
}
fn default_dpi() -> f64 {
DEFAULT_DPI
}
impl Drawing {
/// A drawing of no designs yet, named `name` and written in `language`, with one sheet named
/// `sheet`: the paper `paper` with a copy of `border`, its id from `id_source`.
pub fn new(
name: &str,
language: &str,
sheet: &str,
paper: drawing_sheet::Sheet,
border: Option<DrawingBorderProfile>,
id_source: &mut IdSource,
) -> Result<Drawing> {
let id = id_source.next(&BTreeSet::new())?;
let first = Sheet { id, name: sheet.into(), sheet: paper, border, dpi: DEFAULT_DPI, documents: None, excluded: Excluded::default() };
let drawing = Drawing {
format: DRAWING.format_string(DRAWING.current()),
format_pending: DRAWING.pending_names(),
name: name.into(),
sources: Vec::new(),
properties: Metadata::default(),
units: Units::default(),
language: language.into(),
sheets: vec![Arc::new(first)],
};
drawing.validate()?;
Ok(drawing)
}
/// Read a drawing file's JSON (any format this build knows; older ones are upgraded). A file
/// that breaks a rule of the record is damaged and refused, never patched.
pub fn from_json(s: &str) -> Result<Drawing> {
if s.len() > MAX_DRAWING_BYTES {
return Err(DocError::Invalid(format!("drawing too large (at most {} MiB)", MAX_DRAWING_BYTES >> 20)));
}
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft drawing: {e}")))?;
let (version, applied) = DRAWING.take_version(&mut v)?;
DRAWING.check_numbers(&v, "properties")?;
DRAWING.upgrade(&mut v, version, applied.as_deref())?;
let d: Drawing = serde_json::from_value(v).map_err(damaged)?;
d.validate().map_err(damaged)?;
Ok(Drawing { format: DRAWING.format_string(DRAWING.current()), format_pending: DRAWING.pending_names(), ..d })
}
/// The drawing's JSON: the same bytes for the same drawing. JSON longer than a read takes
/// ([`MAX_DRAWING_BYTES`]) is refused, so every drawing saved opens again: other tools'
/// properties are kept as they came, and indenting them can make a drawing that read in
/// under the limit save over it.
pub fn to_json(&self) -> Result<String> {
let text = serde_json::to_string_pretty(self).map_err(|e| DocError::Invalid(format!("drawing not saved: {e}")))?;
require(text.len() <= MAX_DRAWING_BYTES, || format!("drawing too large to save (at most {} MiB)", MAX_DRAWING_BYTES >> 20))?;
Ok(text)
}
/// An id for a new object: one no object of the drawing has, from `id_source`, which never
/// gives one twice.
pub fn allocate_id(&self, id_source: &mut IdSource) -> Result<u64> {
id_source.next(&self.ids().collect())
}
/// Every object's id, of every kind: what a new id must differ from. Each later kind adds its
/// own here.
fn ids(&self) -> impl Iterator<Item = u64> {
self.sources.iter().map(|s| s.id).chain(self.sheets.iter().map(|s| s.id))
}
/// The record's rules (the module's notes): what reading a file checks, and what every edit
/// keeps.
pub fn validate(&self) -> Result<()> {
require((1..=MAX_SHEETS).contains(&self.sheets.len()), || format!("a drawing has 1 to {MAX_SHEETS} sheets"))?;
require(self.sources.len() <= MAX_SOURCES, || format!("a drawing draws at most {MAX_SOURCES} designs"))?;
check_name(&self.name, "the drawing's")?;
require(is_language_tag(&self.language), || {
format!("the language is not a BCP 47 tag of 2 to {MAX_LANGUAGE_CHARS} characters (such as `en` or `en-GB`)")
})?;
if let Units::Fixed { unit } = &self.units {
// A length unit of the expression unit table, written as the table writes it.
let length = unit.trim() == unit.as_str() && expr::unit_info(unit).is_some_and(|(kind, _)| kind == expr::Kind::Length);
require(length, || "the drawing's unit is not a length unit (such as `mm` or `in`)".into())?;
}
let mut seen = BTreeSet::new();
for id in self.ids() {
require(id != 0, || "an object has the id 0, which is never one".into())?;
require(seen.insert(id), || format!("two objects have the id {}", ids::encode(id)))?;
}
for source in &self.sources {
let path = &source.path;
require(!path.is_empty() && path.len() <= MAX_PATH_BYTES && !path.contains('\0'), || {
format!("source {}: a design's path is 1 to {MAX_PATH_BYTES} bytes, without NUL", ids::encode(source.id))
})?;
}
for sheet in &self.sheets {
self.check_sheet(sheet).map_err(|e| DocError::Invalid(format!("sheet {}: {e}", ids::encode(sheet.id))))?;
}
Ok(())
}
/// A sheet's own rules, and that the source it documents is one of the drawing's.
fn check_sheet(&self, sheet: &Sheet) -> Result<()> {
check_name(&sheet.name, "its")?;
sheet.sheet.size_mm().map_err(from_sheet)?;
if let Some(border) = &sheet.border {
border.validate().map_err(from_sheet)?;
}
require((MIN_DPI..=MAX_DPI).contains(&sheet.dpi), || format!("its pictures are {MIN_DPI} to {MAX_DPI} dpi"))?;
match sheet.documents {
Some(SheetComponent { source, .. }) => {
let named = || format!("it documents source {}, which the drawing doesn't have", ids::encode(source));
require(self.sources.iter().any(|s| s.id == source), named)
}
None => Ok(()),
}
}
}
/// `Ok` when `ok`, else the rule `broken` names.
fn require(ok: bool, broken: impl FnOnce() -> String) -> Result<()> {
if ok { Ok(()) } else { Err(DocError::Invalid(broken())) }
}
/// A name: 1 to [`MAX_NAME_BYTES`] bytes, not only spaces, without control characters.
fn check_name(name: &str, whose: &str) -> Result<()> {
require(!name.trim().is_empty() && name.len() <= MAX_NAME_BYTES && !name.chars().any(char::is_control), || {
format!("{whose} name is 1 to {MAX_NAME_BYTES} bytes, not only spaces, without control characters")
})
}
/// A well-formed language tag, as far as a drawing needs (RFC 5646 §2.1): 2 to
/// [`MAX_LANGUAGE_CHARS`] characters, subtags of 1 to 8 ASCII letters and digits joined by `-`,
/// the first of 2 to 8 letters (`en`, `en-GB`, `zh-Hans-CN`). Whether the subtags are registered
/// isn't checked, and a private-use tag alone (`x-…`) isn't taken.
fn is_language_tag(tag: &str) -> bool {
let mut subtags = tag.split('-');
let first = subtags.next().unwrap_or_default();
(2..=MAX_LANGUAGE_CHARS).contains(&tag.len())
&& (2..=8).contains(&first.len())
&& first.bytes().all(|b| b.is_ascii_alphabetic())
&& subtags.all(|s| (1..=8).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_alphanumeric()))
}
/// A file that breaks a rule of the record.
fn damaged(e: impl std::fmt::Display) -> DocError {
DocError::Invalid(format!("damaged drawing: {e}"))
}
fn from_sheet(e: drawing_sheet::Invalid) -> DocError {
DocError::Invalid(e.to_string())
}
#[cfg(test)]
#[path = "drawing_tests.rs"]
mod tests;
@@ -0,0 +1,30 @@
# Pending drawing-format changes
One file per change to what a drawing file saves that no release has shipped yet. Drawings
(`"format": "solidcraft-drawing/N"`, #3890) are versioned by the same code as designs
(`crates/doc/src/format.rs`, `Family`), from their own first format, 1. Adding a file here is the
whole format bump: never edit the `DRAWING` family in `format.rs`, another change's file or a
`../v<N>/` folder.
- Name it after the change, lowercase with dashes: `sheet-views.rs`.
- Start it with a `//!` note: what is saved, what an older drawing gets without it, and what
the last release does with a drawing that has it (it refuses the newer format number).
- If older drawings must be rewritten as they open, add the step to the same file:
```rust
//! …the note…
use serde_json::Value;
pub fn step(v: &mut Value) -> crate::Result<()> {
// rewrite the drawing's JSON here
Ok(())
}
```
Steps run in file-name order, after the steps of every older format.
While this folder has a change in it, the build writes the next drawing format number (the last
folded one + 1, which no release reads), and lists the changes a drawing has in its
`format_pending` field, so a later build applies only the steps that drawing is missing. At
release time `scripts/fold-format-pending.sh` moves these files into `../v<N>/`, as it does the
design format's (`crates/doc/src/format/pending/README.md`).
+553
View File
@@ -0,0 +1,553 @@
use serde_json::json;
use solidcraft_geom::drawing_sheet::{Margins, Orientation, PaperSize, SheetSize};
use super::*;
/// The seed every test drawing's ids come from.
const SEED: u64 = 3890;
/// A profile of its own: ISO 5457's filing margin and trimming marks, which the built-in leaves off.
fn own_border() -> DrawingBorderProfile {
DrawingBorderProfile { filing_margin: true, trimming_mark_mm: Some([10.0, 5.0]), ..DrawingBorderProfile::iso() }
}
/// Three designs on two sheets: the first sheet (A3, the ISO border) documents component 7 of the
/// first design; the second (A4 portrait, a border of its own, 600 dpi, left out of printing)
/// documents the second design. The third design is on no sheet yet.
fn plate() -> Drawing {
let mut id_source = IdSource::seeded(SEED);
let a3 = drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape);
let mut d = Drawing::new("Plate assembly", "en-GB", "Assembly", a3, Some(DrawingBorderProfile::iso()), &mut id_source).unwrap();
for path in ["plate.sc", "../parts/bolt.sc", "bracket.sc"] {
let id = d.allocate_id(&mut id_source).unwrap();
d.sources.push(Source { id, path: path.into() });
}
let (plate_id, bolt_id) = (d.sources[0].id, d.sources[1].id);
let id = d.allocate_id(&mut id_source).unwrap();
d.sheets.push(Arc::new(Sheet {
id,
name: "Bolt".into(),
sheet: drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Portrait),
border: Some(own_border()),
dpi: 600.0,
documents: Some(SheetComponent { source: bolt_id, component: 0 }),
excluded: Excluded { count: false, print: true },
}));
Arc::make_mut(&mut d.sheets[0]).documents = Some(SheetComponent { source: plate_id, component: 7 });
d.properties.set(crate::metadata::Owner::Design, "author", json!("Jane Doe")).unwrap();
d.units = Units::Fixed { unit: "in".into() };
d.validate().unwrap();
d
}
/// One A4 sheet that documents nothing, no sources: the base the caps grow from.
fn bare() -> Drawing {
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
Drawing::new("Bare", "en", "Sheet", a4, None, &mut IdSource::seeded(SEED)).unwrap()
}
fn file(d: &Drawing) -> Value {
serde_json::from_str(&d.to_json().unwrap()).unwrap()
}
fn read(file: &Value) -> Result<Drawing> {
Drawing::from_json(&file.to_string())
}
/// Why `file` is refused, or "read".
fn refusal(file: &Value) -> String {
read(file).map_or_else(|e| e.to_string(), |_| "read".into())
}
/// Why `d`'s file, changed by `edit`, is refused, or "read".
fn refused(d: &Drawing, edit: impl FnOnce(&mut Value)) -> String {
let mut v = file(d);
edit(&mut v);
refusal(&v)
}
/// A drawing reads back as it was saved, and save, open and save again writes the same bytes, as
/// does another run from the same seed. The file holds each field under the record's name for it,
/// ids as their 13 characters, and no counter.
#[test]
fn a_drawing_reads_back_as_saved_and_saves_the_same_bytes_again() {
let d = plate();
let saved = d.to_json().unwrap();
let reopened = Drawing::from_json(&saved).unwrap();
assert_eq!(reopened, d);
assert_eq!(reopened.to_json().unwrap(), saved);
assert_eq!(plate().to_json().unwrap(), saved, "a seeded run makes the same drawing");
let v = file(&d);
let [plate_id, bolt_id, bracket_id] = [0, 1, 2].map(|i| ids::encode(d.sources[i].id));
let [assembly_id, bolt_sheet_id] = [0, 1].map(|i| ids::encode(d.sheets[i].id));
assert_eq!(assembly_id, "1ge30wkvq38r0", "the seed's first id, as splitmix64 gives it");
assert_eq!(v["format"], DRAWING.format_string(DRAWING.current()));
assert_eq!(v.get("format_pending").is_some(), !DRAWING.pending_names().is_empty());
assert_eq!(v.get("next_id"), None);
assert_eq!((&v["name"], &v["language"]), (&json!("Plate assembly"), &json!("en-GB")));
assert_eq!(
v["sources"],
json!([{"id": plate_id, "path": "plate.sc"}, {"id": bolt_id, "path": "../parts/bolt.sc"}, {"id": bracket_id, "path": "bracket.sc"}])
);
assert_eq!(v["properties"], json!({"author": "Jane Doe"}));
assert_eq!(v["units"], json!({"fixed": {"unit": "in"}}));
assert_eq!((&v["sheets"][0]["id"], &v["sheets"][0]["dpi"]), (&json!(assembly_id), &json!(300.0)));
assert_eq!(v["sheets"][0]["sheet"], json!({"size": {"standard": "a3"}, "orientation": "landscape"}));
assert_eq!(v["sheets"][0]["documents"], json!({"source": plate_id, "component": 7}));
assert_eq!(v["sheets"][0]["excluded"], json!({"count": false, "print": false}));
assert_eq!(
v["sheets"][1],
json!({
"id": bolt_sheet_id, "name": "Bolt", "sheet": {"size": {"standard": "a4"}, "orientation": "portrait"},
"border": serde_json::to_value(own_border()).unwrap(), "dpi": 600.0,
"documents": {"source": bolt_id, "component": 0}, "excluded": {"count": false, "print": true}
})
);
}
/// A hand-written file may leave out what has a default, and reads as a new drawing has it; the
/// rest is needed.
#[test]
fn a_file_may_leave_out_what_has_a_default() {
let minimal = json!({
"format": DRAWING.format_string(1),
"name": "Bracket",
"sources": [],
"language": "de",
"sheets": [{"id": "0000000000001", "name": "Blatt 1", "sheet": {"size": {"standard": "a3"}}}]
});
let d = read(&minimal).unwrap();
assert_eq!((d.sheets[0].id, &d.units, d.properties.is_empty()), (1, &Units::FollowModel, true));
let sheet = &d.sheets[0];
assert_eq!(sheet.sheet, drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape));
assert_eq!((sheet.border, sheet.dpi, sheet.documents, sheet.excluded), (None, DEFAULT_DPI, None, Excluded::default()));
// The format as this build writes it, however the file spelt it.
let mut spelt = minimal.clone();
spelt["format"] = json!("solidcraft-drawing/+1");
assert_eq!(read(&spelt).unwrap().format, DRAWING.format_string(DRAWING.current()));
for key in ["name", "sources", "language", "sheets"] {
let mut v = minimal.clone();
v.as_object_mut().unwrap().remove(key);
let why = refusal(&v);
assert!(why.starts_with("damaged drawing: ") && why.contains(&format!("missing field `{key}`")), "{why}");
}
}
/// Every cap on both sides: sheets, sources, the bytes of paths and names (two-byte characters too).
#[test]
fn every_cap_holds_on_both_sides() {
let base = file(&bare());
let with = |key: &str, value: Value| {
let mut v = base.clone();
v[key] = value;
v
};
let sheet = base["sheets"][0].clone();
let sheets = |n: u64| {
let one = |id: u64| {
let mut s = sheet.clone();
s["id"] = json!(ids::encode(id));
s
};
Value::Array((1..=n).map(one).collect())
};
assert_eq!(read(&with("sheets", sheets(1000))).unwrap().sheets.len(), 1000);
for n in [0, 1001] {
assert_eq!(refusal(&with("sheets", sheets(n))), "damaged drawing: a drawing has 1 to 1000 sheets", "{n} sheets");
}
// Sources from id 1: the sheet's id is the seed's, far from them.
let sources = |n: u64| Value::Array((1..=n).map(|id| json!({"id": ids::encode(id), "path": format!("part{id}.sc")})).collect());
assert_eq!(read(&with("sources", sources(64))).unwrap().sources.len(), 64);
assert_eq!(refusal(&with("sources", sources(65))), "damaged drawing: a drawing draws at most 64 designs");
let path = |p: &str| refusal(&with("sources", json!([{"id": ids::encode(1), "path": p}])));
for fits in ["a".repeat(4096), "é".repeat(2048), "/".into()] {
assert_eq!(path(fits.as_str()), "read", "{} bytes", fits.len());
}
let rule = "damaged drawing: source 0000000000001: a design's path is 1 to 4096 bytes, without NUL";
for broken in ["a".repeat(4097), "é".repeat(2048) + "a", String::new(), "plate\0.sc".into()] {
assert_eq!(path(broken.as_str()), rule, "{} bytes", broken.len());
}
let names = |name: &str| {
let mut sheet_named = base.clone();
sheet_named["sheets"][0]["name"] = json!(name);
(refusal(&with("name", json!(name))), refusal(&sheet_named))
};
for fits in ["x".repeat(256), "é".repeat(128), "A".into(), "Blatt 1 – Übersicht".into()] {
assert_eq!(names(fits.as_str()), ("read".to_string(), "read".to_string()), "{} bytes", fits.len());
}
let rule = "name is 1 to 256 bytes, not only spaces, without control characters";
let sheet_id = ids::encode(bare().sheets[0].id);
for broken in ["x".repeat(257), "é".repeat(128) + "a", String::new(), " ".into(), "two\nlines".into(), "bell\u{7}".into(), "nul\0".into()] {
let expected = (format!("damaged drawing: the drawing's {rule}"), format!("damaged drawing: sheet {sheet_id}: its {rule}"));
assert_eq!(names(broken.as_str()), expected, "{broken:?}");
}
}
/// The content language is a well-formed BCP 47 tag of 2 to 35 characters.
#[test]
fn the_language_is_a_bcp_47_tag_of_2_to_35_characters() {
let base = file(&bare());
let why = |tag: &str| {
let mut v = base.clone();
v["language"] = json!(tag);
refusal(&v)
};
let longest = "en-abcdefgh-abcdefgh-abcdefgh-abcde";
assert_eq!(longest.len(), 35);
for tag in ["en", "de", "ast", "en-GB", "EN-gb", "zh-Hans-CN", "sr-Latn-RS", "es-419", longest] {
assert_eq!(why(tag), "read", "{tag}");
}
let rule = "damaged drawing: the language is not a BCP 47 tag of 2 to 35 characters (such as `en` or `en-GB`)";
let too_long = format!("{longest}f");
let broken =
["e", "", too_long.as_str(), "en_GB", "en-", "-en", "en--GB", "e1", "x-private", "en-abcdefghi", "abcdefghi", "日本", "en GB", "en\n"];
for tag in broken {
assert_eq!(why(tag), rule, "{tag:?}");
}
}
/// A fixed unit is a length unit of the expression unit table, written as the table writes it.
#[test]
fn a_fixed_unit_is_a_length_unit_of_the_expression_table() {
let base = file(&bare());
let units = |units: Value| {
let mut v = base.clone();
v["units"] = units;
read(&v).map(|d| d.units)
};
for (unit, kind, _) in expr::UNITS {
assert_eq!(units(json!({"fixed": {"unit": unit}})).is_ok(), kind == expr::Kind::Length, "{unit}");
}
assert_eq!(units(json!({"fixed": {"unit": "in"}})).unwrap(), Units::Fixed { unit: "in".into() });
assert_eq!(units(json!("follow_model")).unwrap(), Units::FollowModel);
let rule = "damaged drawing: the drawing's unit is not a length unit (such as `mm` or `in`)";
for unit in ["furlong", "deg", "°", "none", "", " in", "in ", "IN"] {
assert_eq!(units(json!({"fixed": {"unit": unit}})).unwrap_err().to_string(), rule, "{unit:?}");
}
for broken in [json!("follow_design"), json!({"fixed": "in"}), json!({"fixed": {}}), json!(null)] {
assert!(units(broken.clone()).is_err(), "{broken}");
}
}
/// A sheet's border is saved as a copy of its profile, field by field, and read back as saved, so
/// a later built-in never changes it; a profile's name in its place is refused.
#[test]
fn a_border_is_saved_as_a_copy_of_its_profile() {
let v = file(&plate());
assert_eq!(v["sheets"][0]["border"], serde_json::to_value(DrawingBorderProfile::iso()).unwrap());
assert_eq!(v["sheets"][1]["border"], serde_json::to_value(own_border()).unwrap());
assert_eq!(read(&v).unwrap().sheets[1].border, Some(own_border()));
let mut edited = v.clone();
edited["sheets"][0]["border"]["margins"]["left_mm"] = json!(15.0);
let iso = DrawingBorderProfile::iso();
let wider = DrawingBorderProfile { margins: Margins { left_mm: 15.0, ..iso.margins }, ..iso };
assert_eq!(read(&edited).unwrap().sheets[0].border, Some(wider));
edited["sheets"][0]["border"] = json!("iso");
assert!(refusal(&edited).starts_with("damaged drawing: "));
edited["sheets"][0]["border"] = Value::Null;
assert_eq!(read(&edited).unwrap().sheets[0].border, None, "no drawing border");
}
/// What reading refuses: each damage the rules name, refused rather than patched, and files this
/// build can't read.
#[test]
fn damaged_files_are_refused_and_never_patched() {
let d = plate();
let plate_id = ids::encode(d.sources[0].id);
let [assembly_id, bolt_sheet_id] = [0, 1].map(|i| ids::encode(d.sheets[i].id));
// Two objects with one id, of one kind and of two kinds.
assert_eq!(refused(&d, |v| v["sources"][1]["id"] = json!(plate_id)), format!("damaged drawing: two objects have the id {plate_id}"));
assert_eq!(refused(&d, |v| v["sheets"][1]["id"] = json!(assembly_id)), format!("damaged drawing: two objects have the id {assembly_id}"));
assert_eq!(refused(&d, |v| v["sources"][2]["id"] = json!(bolt_sheet_id)), format!("damaged drawing: two objects have the id {bolt_sheet_id}"));
// An id that isn't one, where an object has it and where a reference names it; no number form,
// and no counter.
let not_an_id = "damaged drawing: an id is 13 characters of lowercase Crockford base32, not all 0";
for text in ["0000000000000", "FZZZZZZZZZZZZ", "000000000001", "0000000000001 ", "000000000000o", "g000000000000"] {
assert_eq!(refused(&d, |v| v["sources"][2]["id"] = json!(text)), not_an_id, "{text:?}");
assert_eq!(refused(&d, |v| v["sheets"][1]["documents"]["source"] = json!(text)), not_an_id, "{text:?}");
}
assert!(refused(&d, |v| v["sources"][2]["id"] = json!(5)).starts_with("damaged drawing: invalid type"));
assert!(refused(&d, |v| v["next_id"] = json!(6)).contains("unknown field `next_id`"));
let mut in_memory = d.clone();
in_memory.sources[2].id = 0;
assert_eq!(in_memory.validate().unwrap_err().to_string(), "an object has the id 0, which is never one");
// A reference to nothing, or to an object of another kind; a source a sheet documents removed.
for missing in [ids::encode(99), assembly_id.clone()] {
let expected = format!("damaged drawing: sheet {bolt_sheet_id}: it documents source {missing}, which the drawing doesn't have");
assert_eq!(refused(&d, |v| v["sheets"][1]["documents"]["source"] = json!(missing)), expected);
}
let without_plate = refused(&d, |v| {
v["sources"].as_array_mut().unwrap().remove(0);
});
assert_eq!(without_plate, format!("damaged drawing: sheet {assembly_id}: it documents source {plate_id}, which the drawing doesn't have"));
// The paper, its border and its resolution, through the sheet's own rules.
let custom = |sides: Value| refused(&d, |v| v["sheets"][0]["sheet"]["size"] = json!({"custom": {"sides_mm": sides}}));
assert_eq!(custom(json!([5000.0, 0.5])), "read");
for sides in [json!([5000.1, 100.0]), json!([0.0, 100.0]), json!([-1.0, 100.0])] {
let expected = format!("damaged drawing: sheet {assembly_id}: drawing sheet: a custom side is finite, above 0 and at most 5000 mm");
assert_eq!(custom(sides), expected);
}
let why = custom(json!([1e10, 100.0]));
assert!(why.starts_with("damaged drawing: the number ") && why.ends_with(" is out of range"), "checked before the record: {why}");
let nan = d.to_json().unwrap().replacen("600.0", "NaN", 1);
assert!(Drawing::from_json(&nan).unwrap_err().to_string().starts_with("not a SolidCraft drawing: "), "NaN isn't JSON");
assert_eq!(
refused(&d, |v| v["sheets"][1]["border"]["margins"]["top_mm"] = json!(-1.0)),
format!("damaged drawing: sheet {bolt_sheet_id}: drawing sheet: a margin is finite and 0 to 5000 mm")
);
for dpi in [25.0, 2400.0] {
assert_eq!(refused(&d, |v| v["sheets"][1]["dpi"] = json!(dpi)), "read", "{dpi}");
}
for dpi in [24.9, 2400.1, 0.0, -300.0] {
let expected = format!("damaged drawing: sheet {bolt_sheet_id}: its pictures are 25 to 2400 dpi");
assert_eq!(refused(&d, |v| v["sheets"][1]["dpi"] = json!(dpi)), expected, "{dpi}");
}
// In memory, where NaN can be, the same rules.
let mut in_memory = plate();
Arc::make_mut(&mut in_memory.sheets[1]).dpi = f64::NAN;
assert_eq!(in_memory.validate().unwrap_err().to_string(), format!("sheet {bolt_sheet_id}: its pictures are 25 to 2400 dpi"));
let sheet = Arc::make_mut(&mut in_memory.sheets[1]);
sheet.dpi = DEFAULT_DPI;
sheet.sheet.size = SheetSize::Custom { sides_mm: [f64::NAN, 100.0] };
assert!(in_memory.validate().unwrap_err().to_string().starts_with(&format!("sheet {bolt_sheet_id}: drawing sheet: a custom side")));
// An unknown field anywhere.
let unknown: [fn(&mut Value); 8] = [
|v| v["views"] = json!([]),
|v| v["sources"][0]["name"] = json!("Plate"),
|v| v["sheets"][0]["scale"] = json!(1),
|v| v["sheets"][0]["sheet"]["colour"] = json!("white"),
|v| v["sheets"][0]["border"]["style"] = json!("iso"),
|v| v["sheets"][0]["documents"]["view"] = json!(1),
|v| v["sheets"][0]["excluded"]["export"] = json!(true),
|v| v["units"]["fixed"]["precision"] = json!(2),
];
for (i, edit) in unknown.into_iter().enumerate() {
let why = refused(&d, edit);
assert!(why.starts_with("damaged drawing: ") && why.contains("unknown field"), "{i}: {why}");
}
// A newer build's file is refused as one before anything in it is read: a field this build
// doesn't know, or a number out of its range.
let (current, newer) = (DRAWING.current(), DRAWING.current() + 1);
let why = refused(&d, |v| {
v["format"] = json!(DRAWING.format_string(newer));
v["views"] = json!([]);
v["sheets"][0]["dpi"] = json!(1e10);
});
assert_eq!(
why,
format!(
"this drawing was saved by a newer SolidCraft (file format {newer}; this version reads formats up to {current}): update SolidCraft to open it"
)
);
// A list of pending changes means nothing beside a format made of none; beside the pending
// number, a change this build doesn't know is a newer build's.
let why = refused(&d, |v| v["format_pending"] = json!(["a-later-change"]));
if DRAWING.pending_names().is_empty() {
assert_eq!(why, "read");
} else {
assert!(why.contains("newer SolidCraft"), "{why}");
}
// A design is never read as a drawing, nor a drawing as a design.
let design = crate::Document::new("Plate").to_json();
let design_format = crate::format::format_string(crate::format::FORMAT);
assert_eq!(Drawing::from_json(&design).unwrap_err().to_string(), format!("not a SolidCraft drawing (format `{design_format}`)"));
let drawing_format = DRAWING.format_string(current);
assert_eq!(
crate::Document::from_json(&d.to_json().unwrap()).unwrap_err().to_string(),
format!("not a SolidCraft design (format `{drawing_format}`)")
);
// Not an object, not JSON, nested too deeply, too large.
assert_eq!(Drawing::from_json("[]").unwrap_err().to_string(), "not a SolidCraft drawing (expected a JSON object)");
assert!(Drawing::from_json("").unwrap_err().to_string().starts_with("not a SolidCraft drawing: "));
let deep: Value = serde_json::from_str(&format!("{}1{}", "[".repeat(100), "]".repeat(100))).unwrap();
assert_eq!(refused(&d, |v| v["views"] = deep), "damaged drawing: nested too deeply");
let deeper = format!("{{\"format\": \"{drawing_format}\", \"views\": {}1{}}}", "[".repeat(200), "]".repeat(200));
assert!(Drawing::from_json(&deeper).unwrap_err().to_string().starts_with("not a SolidCraft drawing: "));
let text = d.to_json().unwrap();
let at_cap = text.clone() + &" ".repeat(MAX_DRAWING_BYTES - text.len());
assert_eq!(Drawing::from_json(&at_cap).unwrap(), d);
assert_eq!(Drawing::from_json(&(at_cap + " ")).unwrap_err().to_string(), "drawing too large (at most 16 MiB)");
}
/// A run never gives an id twice: not one in use, and not a removed object's. Only another run from
/// the same seed, which tests and replays start from the same file, can give a removed id again.
#[test]
fn a_run_never_gives_an_id_twice() {
let mut id_source = IdSource::seeded(SEED);
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
let mut d = Drawing::new("Ids", "en", "Sheet", a4, None, &mut id_source).unwrap();
let mut given = BTreeSet::from([d.sheets[0].id]);
for _ in 0..MAX_SOURCES {
let id = d.allocate_id(&mut id_source).unwrap();
d.sources.push(Source { id, path: "part.sc".into() });
assert!(given.insert(id), "{id:#x} given twice");
}
let removed = d.sources.pop().unwrap().id;
for _ in 0..10_000 {
let id = d.allocate_id(&mut id_source).unwrap();
assert!(id != removed && given.insert(id), "{id:#x} given twice");
}
// The same seed, from the start, over a file it made: every draw up to the removed id is in use.
let reopened = Drawing::from_json(&d.to_json().unwrap()).unwrap();
let again = reopened.allocate_id(&mut IdSource::seeded(SEED)).unwrap();
assert!(!reopened.ids().any(|id| id == again));
assert_eq!(again, removed);
}
/// Properties follow the design metadata's rules: other tools' data and a well-known key of another
/// kind are kept as they are, never read as numbers, while the same number elsewhere is refused.
#[test]
fn properties_are_kept_as_they_are() {
let mut v = file(&plate());
v["properties"]["com.example.pdm"] = json!({"huge": 1e300, "ids": [1, 2, 3]});
v["properties"]["revision"] = json!(3);
let d = read(&v).unwrap();
assert_eq!(d.properties.get("com.example.pdm"), Some(&json!({"huge": 1e300, "ids": [1, 2, 3]})));
assert_eq!((d.properties.revision(), d.properties.get("revision")), (None, Some(&json!(3))));
assert_eq!(read(&file(&d)).unwrap(), d);
v["sheets"][0]["dpi"] = json!(1e300);
let why = refusal(&v);
assert!(why.starts_with("damaged drawing: the number ") && why.ends_with(" is out of range"), "{why}");
}
/// A drawing whose JSON would be over the read limit isn't saved. Other tools' properties are kept
/// as they came, and indenting them can grow a file read in under the limit past it, so the save
/// refuses rather than write a file that won't open: every drawing saved opens again.
#[test]
fn a_drawing_too_large_to_reopen_is_not_saved() {
let file = |zeros: usize| {
let items = vec!["0"; zeros].join(",");
let sheet = r#"{"id":"0000000000001","name":"S","sheet":{"size":{"standard":"a4"}}}"#;
let format = DRAWING.format_string(1);
format!(
r#"{{"format":"{format}","name":"A","sources":[],"language":"en","sheets":[{sheet}],"properties":{{"com.example":{{"items":[{items}]}}}}}}"#
)
};
// The case found in review (#3891): 2,000,000 zeros read in at about 4 MB and indent past 16 MiB.
let big = Drawing::from_json(&file(2_000_000)).unwrap();
assert_eq!(big.to_json().unwrap_err().to_string(), "drawing too large to save (at most 16 MiB)");
let small = Drawing::from_json(&file(100_000)).unwrap();
assert_eq!(Drawing::from_json(&small.to_json().unwrap()).unwrap(), small);
}
/// A new drawing has one sheet, its id the source's first, follows its designs' units and renders
/// pictures at 300 dpi; a name, language or border that breaks the rules is refused.
#[test]
fn a_new_drawing_has_one_sheet_and_keeps_the_rules() {
let d = bare();
let first = IdSource::seeded(SEED).next(&BTreeSet::new()).unwrap();
assert_eq!((d.sheets.len(), d.sheets[0].id, d.sheets[0].dpi, &d.units), (1, first, DEFAULT_DPI, &Units::FollowModel));
assert_eq!((d.format, d.format_pending), (DRAWING.format_string(DRAWING.current()), DRAWING.pending_names()));
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
let thin = DrawingBorderProfile { drawing_border_line_mm: 0.0, ..DrawingBorderProfile::iso() };
let broken = [("", "en", "Sheet", None), ("Bare", "en_GB", "Sheet", None), ("Bare", "en", "\n", None), ("Bare", "en", "Sheet", Some(thin))];
for (name, language, sheet, border) in broken {
let made = Drawing::new(name, language, sheet, a4, border, &mut IdSource::seeded(SEED));
assert!(made.is_err(), "{name:?} {language:?} {sheet:?} {border:?}");
}
}
/// Hostile input never panics: a valid file's text cut, overwritten and given lies, and its JSON
/// values replaced, repeated and dropped. Each read gives an error, or a drawing that keeps the
/// rules and reads back as it saves.
#[test]
fn mutated_files_give_an_error_or_a_valid_drawing_never_a_panic() {
let good = plate().to_json().unwrap();
let tree: Value = serde_json::from_str(&good).unwrap();
let nodes = count(&tree) as u64;
let lies = [
json!(null),
json!(0),
json!(-1),
json!(1),
json!(1e10),
json!(u64::MAX),
json!(""),
json!("x".repeat(300)),
json!("0000000000000"),
json!("0000000000001"),
json!("fzzzzzzzzzzzz"),
json!("FZZZZZZZZZZZZ"),
json!([]),
json!({}),
json!(true),
json!("follow_model"),
json!({"fixed": {"unit": "deg"}}),
];
let words = ["NaN", "1e400", "-0", "18446744073709551616", "\"", "{", "]", ",", "null", "\u{0}", "é", "o", "I"];
let mut seed: u64 = 0x9e37_79b9_7f4a_7c15;
let mut rnd = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
let mut read_back = 0;
for i in 0..10_000 {
let text = if i % 2 == 0 {
let mut v = tree.clone();
let lie = &lies[(rnd() % lies.len() as u64) as usize];
mutate(&mut v, &mut ((rnd() % nodes) as usize), lie, rnd() % 3);
v.to_string()
} else {
let mut s = good.clone().into_bytes();
for _ in 0..1 + rnd() % 3 {
// A run of up to 15 bytes taken out, overwritten by one byte or replaced by a lie.
let at = (rnd() % (s.len() as u64 + 1)) as usize;
let cut = ((rnd() % 16) as usize).min(s.len() - at);
let byte = [(rnd() >> 56) as u8];
let put: &[u8] = match rnd() % 3 {
0 => &[],
1 => &byte,
_ => words[(rnd() % words.len() as u64) as usize].as_bytes(),
};
let next = [&s[..at], put, &s[at + cut..]].concat();
s = next;
if rnd() % 8 == 0 {
s.truncate(at);
}
}
String::from_utf8_lossy(&s).into_owned()
};
if let Ok(d) = Drawing::from_json(&text) {
assert!(d.validate().is_ok() && d.ids().all(|id| id != 0), "{text}");
assert_eq!(Drawing::from_json(&d.to_json().unwrap()).unwrap(), d, "{text}");
read_back += 1;
}
}
assert!(read_back > 50, "only {read_back} mutated files read: the mutations hardly reach the record's rules");
}
/// The number of values in `v`, itself included.
fn count(v: &Value) -> usize {
1 + match v {
Value::Array(a) => a.iter().map(count).sum::<usize>(),
Value::Object(o) => o.values().map(count).sum(),
_ => 0,
}
}
/// Change the `pick`th value of `v`, depth first: `how` 1 repeats a list's first element (two
/// objects with one id), 2 drops an object's first field, and otherwise `lie` takes its place.
fn mutate(v: &mut Value, pick: &mut usize, lie: &Value, how: u64) -> bool {
if *pick == 0 {
match (v, how) {
(Value::Array(a), 1) if !a.is_empty() => {
let first = a[0].clone();
a.push(first);
}
(Value::Object(o), 2) if !o.is_empty() => {
let key = o.keys().next().cloned().unwrap_or_default();
o.remove(&key);
}
(v, _) => *v = lie.clone(),
}
return true;
}
*pick -= 1;
match v {
Value::Array(a) => a.iter_mut().any(|x| mutate(x, pick, lie, how)),
Value::Object(o) => o.values_mut().any(|x| mutate(x, pick, lie, how)),
_ => false,
}
}
+1 -1
View File
@@ -2332,7 +2332,7 @@ impl Model {
}
Err(e) => Some(e.to_string().trim_start_matches("the operation failed: ").to_string()),
};
if !self.no_cache {
if !self.no_cache && !watched.cancelled() {
let e = CachedEval {
error: error.clone(),
warning: warning.clone(),
+1 -1
View File
@@ -485,7 +485,7 @@ fn mirror_y(r: &Region2) -> Region2 {
.iter()
.map(|s| match *s {
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep },
other => sheet::map_controls(&other, f),
other => other.map_controls(f),
})
.collect();
Loop2 { segs }.ccw()
+5
View File
@@ -183,6 +183,11 @@ impl Document {
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
}
let indices = self.configuration_face_indices(body, &target)?;
// A copy a pattern or mirror made on the same body carries its seed's certificate,
// which finds the seed's face: the copy is attached by its own history name below.
if !indices.contains(&index) {
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
}
self.coplanar_pieces(body, &indices, |_| true)?;
Ok(target)
});
+350 -77
View File
@@ -232,6 +232,11 @@
//! this build writes the next number (which no release reads), with the names of these changes
//! in `format_pending`, so a later build applies just the steps a file is missing. At release
//! time `scripts/fold-format-pending.sh` moves them into the next `v<N>/`.
//!
//! Drawings (`solidcraft-drawing/N`, #3890) are versioned by the same code ([`Family`]) from their
//! own first format, 1 (the record in [`crate::drawing`]): each later change is one file in
//! `src/drawing/format/`, in `v<N>/` or `pending/` as above, and the release script folds that
//! folder too.
use serde_json::Value;
@@ -244,11 +249,11 @@ pub const FORMAT: u32 = FORMATS.current();
/// of [`Part`]s (`src/format/`, listed by `build.rs`).
const HAND_WRITTEN: u32 = 22;
/// One change to the file format: a file in `src/format/v<N>/` or `src/format/pending/`.
/// One change to a family's file format: a file in its `v<N>/` or `pending/` folder.
pub(crate) struct Part {
/// The file's name, which a file written while the change was pending lists.
pub name: &'static str,
/// How a design from before the change is rewritten as it opens, if it is.
/// How a file from before the change is rewritten as it opens, if it is.
pub step: Option<JsonStep>,
}
@@ -257,13 +262,102 @@ include!(concat!(env!("OUT_DIR"), "/format_parts.rs"));
/// Every format this build knows and how to upgrade a design through them.
const FORMATS: Formats<'static> = Formats { hand: &JSON_STEPS, folded: FOLDED, pending: PENDING };
/// The names of the pending changes this build writes into a design (`format_pending`).
pub fn pending_names() -> Vec<String> {
FORMATS.pending.iter().map(|p| p.name.to_string()).collect()
/// A kind of file whose format is versioned here: designs, and drawings (#3890). Each family has
/// its own format tag, its own numbers and its own folder of change files (`build.rs` lists them);
/// reading a file's format, refusing a file a newer build saved and the upgrade are the same code
/// for every family.
pub struct Family<'a> {
/// What a file of the family is called in messages.
noun: &'static str,
/// The tag before the number in a file's `format`.
prefix: &'static str,
/// The tag files saved before the rename carry: read, never written (docs/compatibility.md).
legacy_prefix: Option<&'static str>,
/// The format of a file that names none: designs saved before format strings are format 1; a
/// drawing always names its own.
untagged: Option<u32>,
formats: Formats<'a>,
}
/// The upgrade path: the hand-written steps up to [`HAND_WRITTEN`], the folded formats after it
/// (each a list of changes) and the pending changes, which make the next format.
/// Designs (`solidcraft/N`): the upgrade steps written out up to format 22, then the change files
/// in `src/format/`.
pub const DESIGN: Family<'static> =
Family { noun: "design", prefix: "solidcraft/", legacy_prefix: Some(LEGACY_FORMAT_PREFIX), untagged: Some(1), formats: FORMATS };
/// Drawings (`solidcraft-drawing/N`, #3890): format 1 is the first drawing record, and every later
/// change is a file in `src/drawing/format/`.
pub const DRAWING: Family<'static> = Family {
noun: "drawing",
prefix: "solidcraft-drawing/",
legacy_prefix: None,
untagged: None,
formats: Formats { hand: &[], folded: DRAWING_FOLDED, pending: DRAWING_PENDING },
};
impl Family<'_> {
/// The format this build writes: the last folded one, or the next while changes are pending.
pub const fn current(&self) -> u32 {
self.formats.current()
}
/// The names of the pending changes this build writes into a file (`format_pending`).
pub fn pending_names(&self) -> Vec<String> {
self.formats.pending.iter().map(|p| p.name.to_string()).collect()
}
/// The format tag of format `n`.
pub fn format_string(&self, n: u32) -> String {
format!("{}{n}", self.prefix)
}
/// The version in one of this family's format tags; `None` for any other tag, another
/// family's included.
pub fn version_of(&self, s: &str) -> Option<u32> {
s.strip_prefix(self.prefix).or_else(|| self.legacy_prefix.and_then(|p| s.strip_prefix(p)))?.parse().ok()
}
/// Read a file's format and take its `format_pending` out of its JSON, before anything else
/// in it is read: a file that isn't this family's, or that a newer build saved, is refused
/// here, so nothing a newer build saved is judged by this build's rules.
pub fn take_version(&self, v: &mut Value) -> Result<(u32, Option<Vec<String>>)> {
let noun = self.noun;
let not_ours = |what: String| DocError::Invalid(format!("not a SolidCraft {noun} ({what})"));
let Some(obj) = v.as_object() else { return Err(not_ours("expected a JSON object".into())) };
let version = match obj.get("format") {
None => self.untagged.ok_or_else(|| not_ours("no format".into()))?,
Some(Value::String(f)) => self.version_of(f).ok_or_else(|| not_ours(format!("format `{f}`")))?,
Some(_) => return Err(not_ours("format is not a string".into())),
};
let applied = take_names(v, "format_pending", noun)?;
self.formats.check(noun, version, applied.as_deref())?;
Ok((version, applied))
}
/// Bring a file of format `version` up to this build's; `applied` is its `format_pending`
/// (both from [`Family::take_version`]).
pub fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
self.formats.upgrade(self.noun, v, version, applied)
}
/// Every number a file of the family holds is in range ([`check_numbers`]), but for its
/// top-level `kept` field: other tools' data (a design's `metadata`, a drawing's `properties`),
/// kept as it is and never read as a number (#1015); parsing already bounds its nesting.
pub(crate) fn check_numbers(&self, v: &Value, kept: &str) -> Result<()> {
match v {
Value::Object(m) => m.iter().filter(|(k, _)| k.as_str() != kept).try_for_each(|(_, x)| check_numbers(x, self.noun, 1)),
v => check_numbers(v, self.noun, 0),
}
}
}
/// The names of the pending changes this build writes into a design (`format_pending`).
pub fn pending_names() -> Vec<String> {
DESIGN.pending_names()
}
/// A family's upgrade path: the steps written out by hand (designs' up to [`HAND_WRITTEN`]), the
/// folded formats after them (each a list of changes) and the pending changes, which make the
/// next format.
struct Formats<'a> {
hand: &'a [JsonStep],
folded: &'a [(u32, &'a [Part])],
@@ -300,26 +394,41 @@ impl Formats<'_> {
}
}
/// Bring a design of format `version` up to [`Formats::current`]. `applied` is the file's
/// `format_pending`: the changes of its format it already has, when it was written while they
/// were pending; the others' steps run first. Recognize changes across the entire history:
/// a branch's pending change may have landed at a later number. Unknown changes are refused
/// even after folding, and already-applied changes never run again at their later number.
fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
if version == 0 || version > self.current() {
return Err(DocError::Invalid(format!("not a SolidCraft design (format {version})")));
/// Refuse a `noun` of format `version` that this build can't read: format 0, a newer number,
/// or a change `applied` (its `format_pending`) names that this build doesn't have anywhere in
/// its history, which only a newer build's file can. Changes are recognized across the entire
/// history: a branch's pending change may have landed at a later number. A list beside a format
/// that isn't made of changes means nothing (a tool or a test set an older number on a newer
/// file).
fn check(&self, noun: &str, version: u32, applied: Option<&[String]>) -> Result<()> {
let newer = |what: String| DocError::Invalid(format!("this {noun} was saved by a newer SolidCraft ({what}): update SolidCraft to open it"));
let current = self.current();
if version == 0 {
return Err(DocError::Invalid(format!("not a SolidCraft {noun} (format 0)")));
}
// A list beside a format that isn't made of changes means nothing (a tool or a test set
// an older number on a newer file): it is ignored, like any field an upgrade drops.
if version > current {
return Err(newer(format!("file format {version}; this version reads formats up to {current}")));
}
let applied = applied.filter(|_| self.parts(version).is_some()).unwrap_or_default();
match applied.iter().find(|name| !self.knows_part_name(self.canonical_applied_name(version, name))) {
Some(unknown) => Err(newer(format!("file format {version} with `{unknown}`, which this version doesn't know"))),
None => Ok(()),
}
}
/// Bring a `noun` of format `version` up to [`Formats::current`]. `applied` is the file's
/// `format_pending`: the changes of its format it already has, when it was written while they
/// were pending; the others' steps run first. A file [`Formats::check`] refuses is refused,
/// whoever calls, before anything is changed; already-applied changes never run again at
/// their later number.
fn upgrade(&self, noun: &str, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
self.check(noun, version, applied)?;
// A list beside a format that isn't made of changes is ignored, like any field an upgrade
// drops.
let applied = applied.filter(|_| self.parts(version).is_some());
let already_applied =
|part: &Part| applied.is_some_and(|names| names.iter().any(|name| self.canonical_applied_name(version, name) == part.name));
if let Some(applied) = applied {
if let Some(unknown) = applied.iter().find(|name| !self.knows_part_name(self.canonical_applied_name(version, name))) {
return Err(DocError::Invalid(format!(
"this design was saved by a newer SolidCraft (file format {version} with `{unknown}`, which this version doesn't know): update SolidCraft to open it"
)));
}
if applied.is_some() {
for part in self.parts(version).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
part.step.map_or(Ok(()), |step| step(v))?;
}
@@ -338,25 +447,26 @@ impl Formats<'_> {
}
}
/// Take a file's `format_pending` (the pending changes it has) out of its JSON.
fn take_pending(v: &mut Value) -> Result<Option<Vec<String>>> {
let Some(list) = v.as_object_mut().and_then(|o| o.remove("format_pending")) else { return Ok(None) };
let invalid = || DocError::Invalid("damaged design: `format_pending` is not a short list of names".into());
/// Take a short list of names out of a file's JSON: its `format_pending` (the pending changes it
/// has).
fn take_names(v: &mut Value, key: &str, noun: &str) -> Result<Option<Vec<String>>> {
let Some(list) = v.as_object_mut().and_then(|o| o.remove(key)) else { return Ok(None) };
let invalid = || DocError::Invalid(format!("damaged {noun}: `{key}` is not a short list of names"));
let list = list.as_array().filter(|l| l.len() <= 1000).ok_or_else(invalid)?;
list.iter().map(|n| n.as_str().filter(|n| n.len() <= 128).map(str::to_string).ok_or_else(invalid)).collect::<Result<_>>().map(Some)
}
pub fn format_string(n: u32) -> String {
format!("solidcraft/{n}")
DESIGN.format_string(n)
}
/// The format tag of designs saved before the rename. Compatibility shim (docs/compatibility.md):
/// read, never written.
pub const LEGACY_FORMAT_PREFIX: &str = "solvecraft/";
/// The version in a format string (`solidcraft/N`, or the legacy `solvecraft/N`).
/// The version in a design's format string (`solidcraft/N`, or the legacy `solvecraft/N`).
pub fn version_of(s: &str) -> Option<u32> {
s.strip_prefix("solidcraft/").or_else(|| s.strip_prefix(LEGACY_FORMAT_PREFIX))?.parse().ok()
DESIGN.version_of(s)
}
/// JSON-level upgrades up to [`HAND_WRITTEN`]: entry `i` turns format `i + 1` into `i + 2`.
@@ -566,23 +676,7 @@ fn v2_to_v3(v: &mut Value) -> Result<()> {
/// Read a design file's JSON, upgrading older formats.
pub fn read(s: &str) -> Result<Document> {
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft design: {e}")))?;
let Some(obj) = v.as_object() else {
return Err(DocError::Invalid("not a SolidCraft design (expected a JSON object)".into()));
};
let version = match obj.get("format") {
None => 1,
Some(Value::String(f)) => version_of(f).ok_or_else(|| DocError::Invalid(format!("not a SolidCraft design (format `{f}`)")))?,
Some(_) => return Err(DocError::Invalid("not a SolidCraft design (format is not a string)".into())),
};
let applied = take_pending(&mut v)?;
if version == 0 {
return Err(DocError::Invalid("not a SolidCraft design (format 0)".into()));
}
if version > FORMAT {
return Err(DocError::Invalid(format!(
"this design was saved by a newer SolidCraft (file format {version}; this version reads formats up to {FORMAT}): update SolidCraft to open it"
)));
}
let (version, applied) = DESIGN.take_version(&mut v)?;
check_design_numbers(&v)?;
crate::config::validate_rules_json(&v)?;
crate::config::validate_styles_json(&v)?;
@@ -594,7 +688,7 @@ pub fn read(s: &str) -> Result<Document> {
crate::format_offsets::validate_raw(&v)?;
let legacy_wires = validate_wires(&v, version)?;
validate_body_transfers(&v)?;
FORMATS.upgrade(&mut v, version, applied.as_deref())?;
DESIGN.upgrade(&mut v, version, applied.as_deref())?;
let mut d: Document = serde_json::from_value(v).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
check_typed_numbers(&d)?;
let mut legacy_by_feature = std::collections::BTreeMap::<usize, Vec<usize>>::new();
@@ -1060,22 +1154,22 @@ const MAX_REAL: f64 = 1e9;
/// already bounds its nesting. Engineering SI constants have their own bounded admission
/// (e.g. E in Pa): only the exact root user-material card field bypasses the coordinate bound.
fn check_design_numbers(v: &Value) -> Result<()> {
let Some(root) = v.as_object() else { return check_numbers(v, 0) };
let Some(root) = v.as_object() else { return check_numbers(v, "design", 0) };
for (key, value) in root {
if key == "metadata" {
continue;
}
if key != "user_materials" {
check_numbers(value, 1)?;
check_numbers(value, "design", 1)?;
continue;
}
let Some(materials) = value.as_array() else { return check_numbers(value, 1) };
let Some(materials) = value.as_array() else { return check_numbers(value, "design", 1) };
if materials.len() > crate::MAX_USER_MATERIALS {
return Err(DocError::Invalid("document exceeds 1000 user materials".into()));
}
for material in materials {
let Some(fields) = material.as_object() else {
check_numbers(material, 2)?;
check_numbers(material, "design", 2)?;
continue;
};
for (field, data) in fields {
@@ -1084,7 +1178,7 @@ fn check_design_numbers(v: &Value) -> Result<()> {
if field == "physical_card" && !data.is_null() {
crate::physical_material::validate_card(data)?;
} else {
check_numbers(data, 3)?;
check_numbers(data, "design", 3)?;
}
}
}
@@ -1102,21 +1196,22 @@ fn check_typed_numbers(d: &Document) -> Result<()> {
check_design_numbers(&typed)
}
/// Every non-integer number outside separately validated engineering cards is finite and within ±`MAX_REAL` (a damaged or hostile
/// file is refused instead of overflowing the geometry later).
fn check_numbers(v: &Value, depth: usize) -> Result<()> {
/// Every non-integer number in a `noun`'s file, outside separately validated engineering cards,
/// is finite and within ±`MAX_REAL` (a damaged or hostile file is refused instead of overflowing
/// the geometry later).
fn check_numbers(v: &Value, noun: &str, depth: usize) -> Result<()> {
if depth > 64 {
return Err(DocError::Invalid("damaged design: nested too deeply".into()));
return Err(DocError::Invalid(format!("damaged {noun}: nested too deeply")));
}
match v {
Value::Number(n) if !(n.is_u64() || n.is_i64()) => {
let x = n.as_f64().unwrap_or(f64::NAN);
if !(x.is_finite() && x.abs() <= MAX_REAL) {
return Err(DocError::Invalid(format!("damaged design: the number {n} is out of range")));
return Err(DocError::Invalid(format!("damaged {noun}: the number {n} is out of range")));
}
}
Value::Array(a) => a.iter().try_for_each(|x| check_numbers(x, depth + 1))?,
Value::Object(m) => m.values().try_for_each(|x| check_numbers(x, depth + 1))?,
Value::Array(a) => a.iter().try_for_each(|x| check_numbers(x, noun, depth + 1))?,
Value::Object(m) => m.values().try_for_each(|x| check_numbers(x, noun, depth + 1))?,
_ => {}
}
Ok(())
@@ -1224,25 +1319,25 @@ mod tests {
assert_eq!(formats.current(), next);
for version in [1, HAND_WRITTEN, next - 1] {
let mut v = serde_json::json!({"features": [], "assembly": {"joints": [{"flip": false}]}});
formats.upgrade(&mut v, version, None).unwrap();
formats.upgrade("design", &mut v, version, None).unwrap();
assert_eq!(trace_of(&v), ["a", "b"], "from format {version}");
// The hand-written steps ran too: format 3's joints were flipped on the way up.
assert_eq!(v["assembly"]["joints"][0]["flip"], Value::Bool(version < 4), "from format {version}");
}
// A design of the pending number written before `b-second` existed gets just that step.
let mut v = serde_json::json!({});
formats.upgrade(&mut v, next, Some(&["a-first".to_string()])).unwrap();
formats.upgrade("design", &mut v, next, Some(&["a-first".to_string()])).unwrap();
assert_eq!(trace_of(&v), ["b"]);
// One that has both, or that this build wrote, needs nothing.
let mut v = serde_json::json!({});
formats.upgrade(&mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
formats.upgrade("design", &mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
assert_eq!(trace_of(&v), Vec::<&str>::new());
// A change this build doesn't know is a newer build's: refused, not guessed at.
let err = formats.upgrade(&mut serde_json::json!({}), next, Some(&["c-later".into()])).unwrap_err();
let err = formats.upgrade("design", &mut serde_json::json!({}), next, Some(&["c-later".into()])).unwrap_err();
assert!(err.to_string().contains("newer SolidCraft"), "{err}");
// A list on a format that isn't made of changes means nothing.
let mut v = serde_json::json!({});
formats.upgrade(&mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
formats.upgrade("design", &mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
assert_eq!(trace_of(&v), ["a", "b"]);
}
@@ -1257,13 +1352,13 @@ mod tests {
let formats = Formats { hand: &JSON_STEPS, folded: &folded, pending: &[] };
assert_eq!(formats.current(), next);
let mut old = serde_json::json!({});
formats.upgrade(&mut old, HAND_WRITTEN, None).unwrap();
formats.upgrade("design", &mut old, HAND_WRITTEN, None).unwrap();
assert_eq!(trace_of(&old), ["a", "b"]);
let mut nightly = serde_json::json!({});
formats.upgrade(&mut nightly, next, Some(&["a-first".into()])).unwrap();
formats.upgrade("design", &mut nightly, next, Some(&["a-first".into()])).unwrap();
assert_eq!(trace_of(&nightly), ["b"]);
let mut released = serde_json::json!({});
formats.upgrade(&mut released, next, None).unwrap();
formats.upgrade("design", &mut released, next, None).unwrap();
assert_eq!(trace_of(&released), Vec::<&str>::new());
}
@@ -1276,7 +1371,7 @@ mod tests {
let folded = Formats { hand: &hand, folded: &[(29, &first), (30, &cards)], pending: &[] };
for formats in [pending, folded] {
let mut file = serde_json::json!({"trace": ["b"]});
formats.upgrade(&mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
formats.upgrade("design", &mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
assert_eq!(trace_of(&file), ["b", "a"], "never replay existing card semantics");
}
}
@@ -1288,7 +1383,7 @@ mod tests {
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &[] };
let mut file = serde_json::json!({"trace": [], "sentinel": 7});
let original = file.clone();
let error = formats.upgrade(&mut file, 29, Some(&["physical-material-cards".into()])).unwrap_err();
let error = formats.upgrade("design", &mut file, 29, Some(&["physical-material-cards".into()])).unwrap_err();
assert!(error.to_string().contains("physical-material-cards"), "{error}");
assert_eq!(file, original);
}
@@ -1303,7 +1398,7 @@ mod tests {
for version in [28, 30] {
let mut file = serde_json::json!({"trace": []});
let original = file.clone();
let error = formats.upgrade(&mut file, version, Some(&["physical-material-cards".into()])).unwrap_err();
let error = formats.upgrade("design", &mut file, version, Some(&["physical-material-cards".into()])).unwrap_err();
assert!(error.to_string().contains("physical-material-cards"), "{error}");
assert_eq!(file, original);
}
@@ -1320,7 +1415,7 @@ mod tests {
{
let mut file = serde_json::json!({"trace": ["b"]});
let original = file.clone();
let error = formats.upgrade(&mut file, 29, Some(&names)).unwrap_err();
let error = formats.upgrade("design", &mut file, 29, Some(&names)).unwrap_err();
assert!(error.to_string().contains("unknown-card-codec"), "{error}");
assert_eq!(file, original);
}
@@ -1337,7 +1432,7 @@ mod tests {
vec!["material-engineering-cards".into(), "physical-material-cards".into()],
] {
let mut file = serde_json::json!({"trace": ["b"]});
formats.upgrade(&mut file, 29, Some(&names)).unwrap();
formats.upgrade("design", &mut file, 29, Some(&names)).unwrap();
assert_eq!(trace_of(&file), ["b", "a"]);
}
}
@@ -1350,7 +1445,7 @@ mod tests {
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &cards };
let mut file = serde_json::json!({"trace": ["a"], "metadata": {"format_pending": ["physical-material-cards"]}});
let metadata = file["metadata"].clone();
formats.upgrade(&mut file, 29, None).unwrap();
formats.upgrade("design", &mut file, 29, None).unwrap();
assert_eq!(trace_of(&file), ["a", "b"]);
assert_eq!(file["metadata"], metadata);
}
@@ -1365,7 +1460,7 @@ mod tests {
let formats = Formats { hand: &[], folded: &[(2, &parts)], pending };
let mut file = serde_json::json!({"trace": []});
let original = file.clone();
let err = formats.upgrade(&mut file, 2, Some(&["experimental-card".into()])).unwrap_err();
let err = formats.upgrade("design", &mut file, 2, Some(&["experimental-card".into()])).unwrap_err();
assert!(err.to_string().contains("experimental-card") && err.to_string().contains("update SolidCraft"), "{err}");
assert_eq!(file, original, "refuse before applying even a known migration");
}
@@ -1381,14 +1476,192 @@ mod tests {
let folded = Formats { hand: &[], folded: &[(2, &first), (3, &later)], pending: &[] };
for formats in [pending, folded] {
let mut experimental = serde_json::json!({"trace": ["b"]});
formats.upgrade(&mut experimental, 2, Some(&["b-second".into()])).unwrap();
formats.upgrade("design", &mut experimental, 2, Some(&["b-second".into()])).unwrap();
assert_eq!(trace_of(&experimental), ["b", "a"]);
let mut released = serde_json::json!({"trace": ["a"]});
formats.upgrade(&mut released, 2, None).unwrap();
formats.upgrade("design", &mut released, 2, None).unwrap();
assert_eq!(trace_of(&released), ["a", "b"], "ordinary released files still get later migrations");
}
}
/// A design naming a pending change this build doesn't have is a newer build's, refused as
/// such before anything else in it is checked (as a newer number is), so a newer build's
/// fields never read as damage.
#[test]
fn a_newer_builds_pending_change_is_refused_before_the_design_is_checked() {
let mut file: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap();
file["format_pending"] = serde_json::json!(["not-a-change-anyone-made"]);
file["features"] = serde_json::json!([{"type": "emboss", "face": {"body": "Body", "at": [0, 0, 0], "a_newer_key": true}}]);
let error = read(&file.to_string()).unwrap_err().to_string();
if PENDING.is_empty() {
assert!(error.contains("emboss face"), "a list beside a folded format means nothing: {error}");
} else {
assert_eq!(
error,
format!(
"this design was saved by a newer SolidCraft (file format {FORMAT} with `not-a-change-anyone-made`, which this version doesn't know): update SolidCraft to open it"
)
);
}
}
/// Drawings follow the same rules from their own first format: folded formats follow on from 1
/// (`v2/`, `v3/`, …), the next number is written while changes are pending, and the build lists
/// every change file in the drawing's folder, in file-name order.
#[test]
fn drawing_formats_follow_on_from_one_and_every_change_file_is_listed() {
for (i, (version, parts)) in DRAWING_FOLDED.iter().enumerate() {
assert_eq!(*version, 2 + i as u32);
assert!(!parts.is_empty(), "drawing format {version} has no changes");
}
assert_eq!(DRAWING.current(), 1 + DRAWING_FOLDED.len() as u32 + u32::from(!DRAWING_PENDING.is_empty()));
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/drawing/format");
let names = |dir: &str| -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(root.join(dir))
.map(|d| d.filter_map(|e| e.ok()?.file_name().to_str()?.strip_suffix(".rs").map(str::to_string)).collect())
.unwrap_or_default();
names.sort();
names
};
assert_eq!(names("pending"), DRAWING.pending_names());
for (version, parts) in DRAWING_FOLDED {
assert_eq!(names(&format!("v{version}")), parts.iter().map(|p| p.name).collect::<Vec<_>>());
}
}
/// A design's format tag never reads as a drawing's, nor a drawing's as a design's, at any
/// number: no family's tag starts another's, the legacy tag is a design's only, and each family
/// refuses the other's files by name.
#[test]
fn a_design_never_reads_as_a_drawing_or_the_other_way_round() {
let families = [&DESIGN, &DRAWING];
for a in families {
assert!(a.prefix.len() > 1 && a.prefix.ends_with('/'), "{}", a.prefix);
for b in families.into_iter().filter(|b| b.prefix != a.prefix) {
assert!(!a.prefix.starts_with(b.prefix), "{} starts {}", b.prefix, a.prefix);
}
}
for n in [0, 1, FORMAT, FORMAT + 1, DRAWING.current(), DRAWING.current() + 1, u32::MAX] {
assert_eq!(DESIGN.version_of(&DESIGN.format_string(n)), Some(n));
assert_eq!(DRAWING.version_of(&DRAWING.format_string(n)), Some(n));
assert_eq!(DRAWING.version_of(&DESIGN.format_string(n)), None, "{n}");
assert_eq!(DESIGN.version_of(&DRAWING.format_string(n)), None, "{n}");
assert_eq!(DRAWING.version_of(&format!("{LEGACY_FORMAT_PREFIX}{n}")), None, "{n}: the legacy tag is a design's");
}
for tag in [
"",
"solidcraft-drawing/",
"solidcraft-drawing",
"solidcraft-drawing/x",
"solidcraft-drawing/-1",
"solidcraft-drawing/4294967296",
"Solidcraft-drawing/1",
"solidcraft-drawing/1 ",
"solidcraft-drawings/1",
"solidcraft/drawing/1",
] {
assert_eq!(DRAWING.version_of(tag), None, "{tag:?}");
}
// The number reads as Rust reads a `u32`, a leading `+` included, as in a design's tag.
assert_eq!(DRAWING.version_of("solidcraft-drawing/+1"), Some(1));
let error = DRAWING.take_version(&mut serde_json::json!({"format": format_string(FORMAT)})).unwrap_err().to_string();
assert_eq!(error, format!("not a SolidCraft drawing (format `solidcraft/{FORMAT}`)"));
let error = read(&serde_json::json!({"format": DRAWING.format_string(1)}).to_string()).unwrap_err().to_string();
assert_eq!(error, "not a SolidCraft design (format `solidcraft-drawing/1`)");
}
/// Every refusal names its family: a design's read exactly as they did before drawings had one
/// (the troubleshooting guide quotes them), a drawing's say "drawing". A file that names no
/// format is a design from before format strings, never a drawing.
#[test]
fn refusals_name_the_family_and_a_designs_read_as_before() {
use serde_json::json;
fn refusal(family: &Family<'_>, mut file: Value) -> String {
family.take_version(&mut file).map_or_else(|e| e.to_string(), |opened| format!("read as {opened:?}"))
}
fn tagged(family: &Family<'_>, n: u32) -> Value {
serde_json::json!({"format": family.format_string(n)})
}
fn newer(noun: &str, n: u32, current: u32) -> String {
format!(
"this {noun} was saved by a newer SolidCraft (file format {n}; this version reads formats up to {current}): update SolidCraft to open it"
)
}
assert_eq!(refusal(&DESIGN, tagged(&DESIGN, FORMAT + 1)), newer("design", FORMAT + 1, FORMAT));
assert_eq!(read(&tagged(&DESIGN, FORMAT + 1).to_string()).unwrap_err().to_string(), newer("design", FORMAT + 1, FORMAT));
let current = DRAWING.current();
assert!(matches!(DRAWING.take_version(&mut tagged(&DRAWING, current)), Ok((v, None)) if v == current));
for n in [current + 1, u32::MAX] {
assert_eq!(refusal(&DRAWING, tagged(&DRAWING, n)), newer("drawing", n, current));
}
for (family, noun) in [(&DESIGN, "design"), (&DRAWING, "drawing")] {
assert_eq!(refusal(family, tagged(family, 0)), format!("not a SolidCraft {noun} (format 0)"));
assert_eq!(refusal(family, json!([])), format!("not a SolidCraft {noun} (expected a JSON object)"));
assert_eq!(refusal(family, json!({"format": 1})), format!("not a SolidCraft {noun} (format is not a string)"));
assert_eq!(refusal(family, json!({"format": ""})), format!("not a SolidCraft {noun} (format ``)"));
// `format_pending`'s caps, on both sides of each.
let mut file = tagged(family, 1);
file["format_pending"] = vec!["x".repeat(128); 1000].into();
assert!(family.take_version(&mut file).is_ok(), "{noun}: 1,000 names of 128 bytes");
for damaged in [vec!["x".repeat(128); 1001], vec!["x".repeat(129)]] {
let mut file = tagged(family, 1);
file["format_pending"] = damaged.into();
assert_eq!(refusal(family, file), format!("damaged {noun}: `format_pending` is not a short list of names"));
}
}
// A change this build doesn't know, in a design of the pending number.
let pending = [traced("a-first")];
let design = Family { formats: Formats { hand: &JSON_STEPS, folded: FOLDED, pending: &pending }, ..DESIGN };
let next = design.current();
let file = json!({"format": format_string(next), "format_pending": ["c-later"]});
assert_eq!(
refusal(&design, file),
format!(
"this design was saved by a newer SolidCraft (file format {next} with `c-later`, which this version doesn't know): update SolidCraft to open it"
)
);
assert!(matches!(DESIGN.take_version(&mut json!({})), Ok((1, None))));
assert_eq!(refusal(&DRAWING, json!({})), "not a SolidCraft drawing (no format)");
}
/// One drawing change pending, as a drawing's first change will be: the build writes format 2
/// and names the change in `format_pending`. Its step runs for a drawing without the change and
/// never for one with it; a drawing naming a change this build lacks, or of format 3, is a newer
/// build's.
#[test]
fn a_pending_drawing_change_makes_format_two() {
use serde_json::json;
let pending = [traced("a-first")];
let drawing = Family { formats: Formats { hand: &[], folded: &[], pending: &pending }, ..DRAWING };
assert_eq!((drawing.current(), drawing.format_string(drawing.current())), (2, "solidcraft-drawing/2".to_string()));
assert_eq!(drawing.pending_names(), ["a-first"]);
let open = |mut file: Value| -> Result<Vec<String>> {
let (version, applied) = drawing.take_version(&mut file)?;
assert!(file.get("format_pending").is_none(), "taken out of the JSON before the record is read");
drawing.upgrade(&mut file, version, applied.as_deref())?;
Ok(trace_of(&file).into_iter().map(str::to_string).collect())
};
assert_eq!(open(json!({"format": "solidcraft-drawing/1"})).unwrap(), ["a"]);
assert_eq!(open(json!({"format": "solidcraft-drawing/2", "format_pending": []})).unwrap(), ["a"]);
assert_eq!(open(json!({"format": "solidcraft-drawing/2", "format_pending": ["a-first"]})).unwrap(), Vec::<String>::new());
// A list beside format 1 means nothing: the step still runs.
assert_eq!(open(json!({"format": "solidcraft-drawing/1", "format_pending": ["c-later"]})).unwrap(), ["a"]);
let error = open(json!({"format": "solidcraft-drawing/2", "format_pending": ["a-first", "c-later"]})).unwrap_err().to_string();
assert_eq!(
error,
"this drawing was saved by a newer SolidCraft (file format 2 with `c-later`, which this version doesn't know): update SolidCraft to open it"
);
let error = open(json!({"format": "solidcraft-drawing/3"})).unwrap_err().to_string();
assert_eq!(
error,
"this drawing was saved by a newer SolidCraft (file format 3; this version reads formats up to 2): update SolidCraft to open it"
);
// An upgrade called without `take_version` refuses in the drawing's words too, format 0 included.
let error = drawing.upgrade(&mut json!({}), 3, None).unwrap_err().to_string();
assert!(error.starts_with("this drawing was saved by a newer SolidCraft (file format 3;"), "{error}");
assert_eq!(drawing.upgrade(&mut json!({}), 0, None).unwrap_err().to_string(), "not a SolidCraft drawing (format 0)");
}
/// `format_pending` is read and checked: a damaged list is refused, a list of changes this
/// build doesn't have is a newer build's, and saving writes this build's own list.
#[test]
@@ -0,0 +1,8 @@
//! Linked components retain the last inherited physical material, explicit body look and
//! authored face overrides by imported body slot. Face baselines are optional: an older
//! link is read again on Update Links while preserving local assignments. Authored face
//! overrides stay separate from imported STEP paint, including under a body appearance.
//! Source-only property edits can then update the link while
//! differing local assignments survive. Older links have no baseline: existing assignments
//! are preserved and missing ones can be recovered on Update Links. The last release
//! refuses the newer design format instead of silently discarding these baselines.

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